<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>X2Q</title>
    <subtitle>X2Q is a personal blog with notes on software, Linux, networking, payments, home labs, cooking, and the odd side project. Online since 2002.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://www.x2q.net/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://www.x2q.net"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-10T00:00:00+00:00</updated>
    <id>https://www.x2q.net/atom.xml</id>
    <entry xml:lang="en">
        <title>BIN&#x2F;IIN lookup: the card prefix ranges and how to look one up (2026)</title>
        <published>2026-06-10T00:00:00+00:00</published>
        <updated>2026-06-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/bin-iin-lookup/"/>
        <id>https://www.x2q.net/post/bin-iin-lookup/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/bin-iin-lookup/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; A card’s first digits — the &lt;strong&gt;BIN&lt;&#x2F;strong&gt; (Bank Identification Number) &#x2F; &lt;strong&gt;IIN&lt;&#x2F;strong&gt; (Issuer Identification Number) — identify the &lt;strong&gt;network&lt;&#x2F;strong&gt; and the &lt;strong&gt;issuer&lt;&#x2F;strong&gt;. You can read the &lt;em&gt;network&lt;&#x2F;em&gt; straight off the prefix ranges (table below). The &lt;em&gt;issuer&#x2F;bank&#x2F;country&lt;&#x2F;em&gt; needs a &lt;strong&gt;BIN database&lt;&#x2F;strong&gt; — there’s no formula for it. This page gives you the &lt;strong&gt;IIN range table&lt;&#x2F;strong&gt;, the &lt;strong&gt;6-vs-8-digit caveat&lt;&#x2F;strong&gt;, and the &lt;strong&gt;three ways to look a BIN up&lt;&#x2F;strong&gt; (by hand, by site&#x2F;API, programmatically).&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Background reading on this blog: &lt;a href=&quot;&#x2F;post&#x2F;8-digit-bin-iin-explained&#x2F;&quot;&gt;why a 6-digit BIN is now ambiguous&lt;&#x2F;a&gt; and &lt;a href=&quot;&#x2F;post&#x2F;binlist-net-iinlist-com-story&#x2F;&quot;&gt;the story behind binlist.net and iinlist.com&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;what-a-bin-iin-is&quot;&gt;What a BIN&#x2F;IIN is&lt;&#x2F;h2&gt;
&lt;p&gt;The card number (PAN, primary account number) is structured: the &lt;strong&gt;leading digits are the IIN&lt;&#x2F;strong&gt;, identifying the issuing institution and network; the rest is the individual account number, with a final &lt;strong&gt;Luhn check digit&lt;&#x2F;strong&gt;. “BIN” is the everyday industry word; “IIN” is the formal term in &lt;strong&gt;ISO&#x2F;IEC 7812&lt;&#x2F;strong&gt;. The IIN was &lt;strong&gt;6 digits&lt;&#x2F;strong&gt; for decades and has been &lt;strong&gt;expanded to 8&lt;&#x2F;strong&gt; for most networks — see &lt;a href=&quot;&#x2F;post&#x2F;8-digit-bin-iin-explained&#x2F;&quot;&gt;the 8-digit post&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;iin-prefix-ranges-by-network&quot;&gt;IIN prefix ranges by network&lt;&#x2F;h2&gt;
&lt;p&gt;What you &lt;em&gt;can&lt;&#x2F;em&gt; determine from the prefix alone — which network issued the card:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Network&lt;&#x2F;th&gt;&lt;th&gt;IIN prefix range(s)&lt;&#x2F;th&gt;&lt;th&gt;PAN length&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Visa&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;4&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;13, 16, 19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mastercard&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;51&lt;&#x2F;code&gt;–&lt;code&gt;55&lt;&#x2F;code&gt;, &lt;code&gt;2221&lt;&#x2F;code&gt;–&lt;code&gt;2720&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;American Express&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;34&lt;&#x2F;code&gt;, &lt;code&gt;37&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Discover&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;6011&lt;&#x2F;code&gt;, &lt;code&gt;644&lt;&#x2F;code&gt;–&lt;code&gt;649&lt;&#x2F;code&gt;, &lt;code&gt;65&lt;&#x2F;code&gt;, &lt;code&gt;622126&lt;&#x2F;code&gt;–&lt;code&gt;622925&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16–19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Diners Club Intl&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;36&lt;&#x2F;code&gt;, &lt;code&gt;300&lt;&#x2F;code&gt;–&lt;code&gt;305&lt;&#x2F;code&gt;, &lt;code&gt;3095&lt;&#x2F;code&gt;, &lt;code&gt;38&lt;&#x2F;code&gt;–&lt;code&gt;39&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;14–19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JCB&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;3528&lt;&#x2F;code&gt;–&lt;code&gt;3589&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16–19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;UnionPay&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;62&lt;&#x2F;code&gt;, &lt;code&gt;81&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16–19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Maestro&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;50&lt;&#x2F;code&gt;, &lt;code&gt;56&lt;&#x2F;code&gt;–&lt;code&gt;69&lt;&#x2F;code&gt;, &lt;code&gt;6304&lt;&#x2F;code&gt;, &lt;code&gt;6759&lt;&#x2F;code&gt;, &lt;code&gt;676770&lt;&#x2F;code&gt;, &lt;code&gt;676774&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;12–19&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;RuPay&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;60&lt;&#x2F;code&gt;, &lt;code&gt;6521&lt;&#x2F;code&gt;, &lt;code&gt;6522&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mir&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;2200&lt;&#x2F;code&gt;–&lt;code&gt;2204&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Troy&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;9792&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Notes: ranges can overlap at the edges (e.g. some &lt;code&gt;62&lt;&#x2F;code&gt; UnionPay vs &lt;code&gt;622126&lt;&#x2F;code&gt;–&lt;code&gt;622925&lt;&#x2F;code&gt; Discover co-badging), and &lt;code&gt;50&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;56&lt;&#x2F;code&gt;–&lt;code&gt;69&lt;&#x2F;code&gt; for Maestro is broad and collides with others — which is exactly why issuer-level resolution needs a database, not a prefix rule.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-you-cannot-tell-from-the-prefix&quot;&gt;What you &lt;em&gt;cannot&lt;&#x2F;em&gt; tell from the prefix&lt;&#x2F;h2&gt;
&lt;p&gt;There is &lt;strong&gt;no formula&lt;&#x2F;strong&gt; that turns a prefix into a bank name, country, or debit&#x2F;credit flag. That mapping lives in a maintained &lt;strong&gt;BIN database&lt;&#x2F;strong&gt; built from network bulletins and observed cards. So:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;&#x2F;strong&gt; → readable from the table above. ✅&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Issuer bank, country, card type, product, prepaid&#x2F;debit&#x2F;credit&lt;&#x2F;strong&gt; → database lookup only. ❌ by hand.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;three-ways-to-look-up-a-bin&quot;&gt;Three ways to look up a BIN&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;1-by-hand-network-only&quot;&gt;1. By hand (network only)&lt;&#x2F;h3&gt;
&lt;p&gt;Take the first digits and match them against the table above. Good enough when all you need is “is this Visa or Mastercard?”.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-on-a-lookup-site-free-api&quot;&gt;2. On a lookup site &#x2F; free API&lt;&#x2F;h3&gt;
&lt;p&gt;For issuer-level detail, use a BIN lookup service. The well-known free option is &lt;strong&gt;binlist.net&lt;&#x2F;strong&gt;, which exposes a JSON API:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;curl https:&#x2F;&#x2F;lookup.binlist.net&#x2F;45717360
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &amp;quot;scheme&amp;quot;: &amp;quot;visa&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;debit&amp;quot;,
  &amp;quot;brand&amp;quot;: &amp;quot;Visa Classic&amp;quot;,
  &amp;quot;bank&amp;quot;: { &amp;quot;name&amp;quot;: &amp;quot;Example Bank&amp;quot;, &amp;quot;country&amp;quot;: &amp;quot;...&amp;quot; },
  &amp;quot;country&amp;quot;: { &amp;quot;name&amp;quot;: &amp;quot;United Kingdom&amp;quot;, &amp;quot;alpha2&amp;quot;: &amp;quot;GB&amp;quot; }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It’s &lt;strong&gt;rate-limited&lt;&#x2F;strong&gt; (a handful of requests per hour) and best for occasional lookups. For volume, 8-digit granularity, and reliability you want a licensed database — &lt;strong&gt;iinlist.com&lt;&#x2F;strong&gt; is the commercial option built for exactly this. (Background: &lt;a href=&quot;&#x2F;post&#x2F;binlist-net-iinlist-com-story&#x2F;&quot;&gt;how both came to exist&lt;&#x2F;a&gt;.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Only ever send the BIN — the first 6-8 digits — never a full card number&lt;&#x2F;strong&gt; to a third-party lookup. The prefix is all a lookup needs, and full PANs are sensitive (PCI) data.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-programmatically&quot;&gt;3. Programmatically&lt;&#x2F;h3&gt;
&lt;p&gt;For more than the occasional check, validate and normalise first, then look up:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;# normalise: strip spaces, take first 8 digits as the BIN
bin=$(echo &amp;quot;4571 7360 1234 5678&amp;quot; | tr -dc &amp;#39;0-9&amp;#39; | cut -c1-8)
curl -s &amp;quot;https:&#x2F;&#x2F;lookup.binlist.net&#x2F;$bin&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A quick &lt;strong&gt;Luhn&lt;&#x2F;strong&gt; validity check before you store or look anything up (POSIX shell):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;luhn() {  # returns 0 if the number passes the Luhn check
  echo &amp;quot;$1&amp;quot; | tr -dc &amp;#39;0-9&amp;#39; | rev | awk &amp;#39;{
    s=0; for(i=1;i&amp;lt;=length;i++){d=substr($0,i,1);
      if(i%2==0){d*=2; if(d&amp;gt;9)d-=9}; s+=d}
    exit (s%10!=0)
  }&amp;#39;
}
luhn &amp;quot;4571736012345678&amp;quot; &amp;amp;&amp;amp; echo valid || echo invalid
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For production BIN matching, load a licensed BIN file into your own table and key on &lt;strong&gt;8 digits&lt;&#x2F;strong&gt;, falling back to 6 with a low-confidence flag.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;BIN&#x2F;IIN&lt;&#x2F;strong&gt; is the card number’s leading digits — &lt;strong&gt;network + issuer&lt;&#x2F;strong&gt;; 6 digits historically, &lt;strong&gt;8 now&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;You can read the &lt;strong&gt;network&lt;&#x2F;strong&gt; off the &lt;a href=&quot;https:&#x2F;&#x2F;www.x2q.net&#x2F;post&#x2F;bin-iin-lookup&#x2F;#iin-prefix-ranges-by-network&quot;&gt;prefix ranges&lt;&#x2F;a&gt;; &lt;strong&gt;issuer&#x2F;bank&#x2F;country needs a database&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Look up: &lt;strong&gt;by hand&lt;&#x2F;strong&gt; (network), a &lt;strong&gt;site&#x2F;API&lt;&#x2F;strong&gt; like binlist.net (free, rate-limited) or &lt;strong&gt;iinlist.com&lt;&#x2F;strong&gt; (commercial, 8-digit), or &lt;strong&gt;programmatically&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Send &lt;strong&gt;only the first 6-8 digits&lt;&#x2F;strong&gt;, never a full PAN, to any third-party lookup.&lt;&#x2F;li&gt;
&lt;li&gt;Prefer &lt;strong&gt;8-digit&lt;&#x2F;strong&gt; matching — see &lt;a href=&quot;&#x2F;post&#x2F;8-digit-bin-iin-explained&#x2F;&quot;&gt;why 6 digits is now ambiguous&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Transcribing 33,000 Danish voice logs on home GPUs — the local pipeline (2026)</title>
        <published>2026-06-10T00:00:00+00:00</published>
        <updated>2026-06-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/danish-voice-log-transcription/"/>
        <id>https://www.x2q.net/post/danish-voice-log-transcription/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/danish-voice-log-transcription/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; Business phone calls had been recorded for over a year: &lt;strong&gt;~33,000 Danish mp3 files, ~570 hours, 32 kbps phone quality&lt;&#x2F;strong&gt;. The brief was to transcribe all of it, name the speakers, summarize per call &#x2F; per day &#x2F; per week, make it browsable — and do it &lt;strong&gt;locally, with no LLM API&lt;&#x2F;strong&gt;. The result is a pipeline that runs &lt;strong&gt;two ASR models and fuses them with Claude&lt;&#x2F;strong&gt;, identifies speakers &lt;strong&gt;from metadata before decoding any audio&lt;&#x2F;strong&gt;, heals itself across &lt;strong&gt;two GPUs&lt;&#x2F;strong&gt;, and uses &lt;strong&gt;Claude Code subagents as the summary layer&lt;&#x2F;strong&gt; instead of an API. Here’s the whole build, and what it teaches about a customer-service function.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The grown-up sequel to &lt;a href=&quot;&#x2F;post&#x2F;local-speech-to-text-whisper-cpp&#x2F;&quot;&gt;running Whisper locally instead of a cloud Speech API&lt;&#x2F;a&gt;. Same instinct — keep the audio on your own box — at 33,000-file scale.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;start-by-analysing-the-corpses&quot;&gt;Start by analysing the corpses&lt;&#x2F;h2&gt;
&lt;p&gt;The folder already held &lt;strong&gt;nine half-finished attempts&lt;&#x2F;strong&gt; at this problem. Before writing anything new, three agents read all nine. Between them, the dead experiments contained almost every good idea the final system needed:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;One Python attempt (WhisperX + pyannote + Claude day-summaries) was the most complete — but had hardcoded API keys, an English-alignment bug on Danish audio, and mock diarization in the “real” pipeline.&lt;&#x2F;li&gt;
&lt;li&gt;One had discovered the key trick: &lt;strong&gt;phone-first speaker identification&lt;&#x2F;strong&gt; (more below), and had already built voice profiles.&lt;&#x2F;li&gt;
&lt;li&gt;A Go rewrite had a beautiful service architecture and a SQLite schema — but &lt;strong&gt;everything was stubs&lt;&#x2F;strong&gt;; zero segments were ever stored. Lesson: never build the shell before the signal path works.&lt;&#x2F;li&gt;
&lt;li&gt;A benchmark harness had already compared &lt;strong&gt;six ASR models&lt;&#x2F;strong&gt; on the corpus.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Synthesising the nine was far cheaper than inventing from scratch. &lt;strong&gt;Lesson one: analyse the corpses first.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-model-benchmark&quot;&gt;The model benchmark&lt;&#x2F;h2&gt;
&lt;p&gt;The pre-existing benchmark (on the RTX 4070 Ti) plus a fresh side-by-side settled the model choice:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;&#x2F;th&gt;&lt;th&gt;Result&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;faster-whisper large-v3-turbo&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Fastest — ~37× real-time, 1.8 GB VRAM. Great, but slips on noisy Danish.&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;faster-whisper large-v3 (full)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Most coherent on real calls. ~18× real-time — fast enough. &lt;strong&gt;Chosen.&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;hviske-v3-conversation&lt;&#x2F;strong&gt; (Danish fine-tune)&lt;&#x2F;td&gt;&lt;td&gt;Catches Danish names&#x2F;idioms the others garble. &lt;strong&gt;Chosen as the second track.&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Voxtral&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Hallucinated in loops (1,832 words for a 117-second call), language-bleed. &lt;strong&gt;Dropped.&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;vibevoice&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;~6× slower than real-time. &lt;strong&gt;Dropped.&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The non-obvious decision: &lt;strong&gt;run two models, not one.&lt;&#x2F;strong&gt; large-v3 and hviske &lt;strong&gt;fail differently&lt;&#x2F;strong&gt; — one has the grammar, the other has the Danish names — so keeping both and fusing them beats either alone.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-pipeline&quot;&gt;The pipeline&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;mp3 → ffmpeg band-pass (200–3400 Hz + light denoise + dynaudnorm) → 16 kHz mono
    → [A] faster-whisper large-v3 (int8_float16, beam 5, VAD, anti-loop guards)
    → [B] hviske-v3-conversation (Danish fine-tune)              ← only on calls ≥45 s
    → ECAPA-TDNN diarization (token-free clustering, 2-speaker prior on long calls)
    → &amp;quot;phone-first&amp;quot; naming (filename + phonebook anchor identities; embeddings
       decide which cluster is the recurring party; reference profiles match the rest)
    → SQLite (idempotent: SHA-256 hash + status; duplicates inherit their twin&amp;#39;s result)

per day  → Claude subagent reads BOTH transcripts per call, fuses them, fixes
           diarization from context, writes the Danish day summary
per week → Claude subagent synthesises 7 day summaries into threads + a commitments table
web      → Flask reads the DB live (days, weeks, calls with audio, search, patterns)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A few decisions earned their place:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phone-first speaker ID.&lt;&#x2F;strong&gt; The filename carries the timestamp and both numbers; a small phonebook maps numbers to names. You know &lt;em&gt;both&lt;&#x2F;em&gt; identities before decoding a word — audio only has to decide &lt;em&gt;which voice is which&lt;&#x2F;em&gt;, not &lt;em&gt;who they are&lt;&#x2F;em&gt;. &lt;strong&gt;Metadata is gold.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ffmpeg band-pass helps; DeepFilterNet hurts.&lt;&#x2F;strong&gt; Measured: a 50-year-old band-pass filter made noisy calls more coherent for free; the SOTA neural denoiser caused hallucinations and dropped speech. &lt;strong&gt;SOTA is task-dependent.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ECAPA over pyannote.&lt;&#x2F;strong&gt; pyannote OOM’d next to Whisper on a 12 GB card and was too slow on CPU for 33k files. Token-free ECAPA clustering on the GPU, with a &lt;strong&gt;2-speaker prior&lt;&#x2F;strong&gt; (a phone call &lt;em&gt;is&lt;&#x2F;em&gt; two-party), fixed the 79% of long calls that otherwise collapsed to a single speaker.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;int8_float16 + expandable segments&lt;&#x2F;strong&gt; let &lt;strong&gt;both&lt;&#x2F;strong&gt; models coexist in ~6–8 GB on the 12 GB card.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-summary-layer-claude-code-subagents-no-api&quot;&gt;The summary layer: Claude Code subagents, no API&lt;&#x2F;h2&gt;
&lt;p&gt;The hard constraint was &lt;strong&gt;no LLM API key&lt;&#x2F;strong&gt;. So the harness itself is the language model: &lt;strong&gt;one Claude Code subagent per day&lt;&#x2F;strong&gt; receives both ASR tracks for every call, a shared name register, and the previous day’s summary, and writes the day summary directly — ~50k–450k tokens per day-agent, &lt;strong&gt;no API call in the pipeline, no marginal cost.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Three small “institutions” grew around it:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A fusion instruction file&lt;&#x2F;strong&gt; — the merge rules ([A]’s structure, [B]’s Danish words), fix diarization from context (a caller stating their own name pins that speaker), skip voicemails, &lt;strong&gt;never invent content, report uncertainty honestly.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A shared, growing name register&lt;&#x2F;strong&gt; — agents read it before writing and append new clarifications. Over 60+ days it reconciles spelling variants of the same contact into one confirmed identity and keeps two same-named people apart. Name consistency across the whole corpus, accreted one day at a time.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A weekly commitments table&lt;&#x2F;strong&gt; — each week’s agent carries a “promises &amp;amp; deliveries” table forward (✅&#x2F;⏳&#x2F;❌). A genuinely useful primitive (and, as we’ll see, the bridge to customer service).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;running-it-across-two-machines&quot;&gt;Running it across two machines&lt;&#x2F;h2&gt;
&lt;p&gt;Idempotency made the corpus splittable mid-run:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Machine 1 (RTX 4070 Ti, 12 GB):&lt;&#x2F;strong&gt; the full dual-model pipeline, working chronologically forward.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Machine 2 (GTX 1060, 6 GB, Pascal):&lt;&#x2F;strong&gt; deployed over SSH&#x2F;Tailscale with one script; took a later partition. Runs only the [A] track in int8 (new CUDA wheels dropped Pascal, so torch is on CPU while CTranslate2 drives the GPU directly).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Merge:&lt;&#x2F;strong&gt; the worker writes its own SQLite; the main box pulls and merges every 15 minutes (idempotent — &lt;code&gt;transcribed&lt;&#x2F;code&gt; can be upgraded to &lt;code&gt;done&lt;&#x2F;code&gt;). The corpus is processed &lt;strong&gt;from both ends at once.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Because every file is keyed by &lt;strong&gt;SHA-256 hash + status&lt;&#x2F;strong&gt;, moving half the work to another machine mid-run was harmless. &lt;strong&gt;Idempotency is the freedom to fail.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;self-healing-learned-the-hard-way&quot;&gt;Self-healing, learned the hard way&lt;&#x2F;h2&gt;
&lt;p&gt;Long unattended runs fail in creative ways. Each failure got a permanent countermeasure:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Two transcription processes on one GPU&lt;&#x2F;strong&gt; (an orphan survived a restart) → mutual OOM poisoning, 756 failed rows. Fix: a &lt;code&gt;flock&lt;&#x2F;code&gt; singleton in the supervisor + auto-kill of orphans on startup.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The “poison file” myth&lt;&#x2F;strong&gt; — a file looked like it crashed the process repeatedly; it was actually the dual-process conflict. It ran fine on retry. The supervisor got retry-once-then-quarantine logic anyway.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Silent hangs&lt;&#x2F;strong&gt; — a worker hung for an hour on one file (process alive, no progress). Fix: a watchdog that kills the inner process after 20 minutes with no DB activity (DB mtime as the progress signal).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;An outer watchdog every 10 minutes&lt;&#x2F;strong&gt; checks the supervisor, DB progress and web server locally — and all of it on the worker over SSH — and auto-restarts.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;False “ready” days&lt;&#x2F;strong&gt; — a max-date heuristic declared a day finished while 93 of its 104 files were still missing. Fix: &lt;strong&gt;the inbox is ground truth&lt;&#x2F;strong&gt; — a day&#x2F;week is ready only when &lt;em&gt;every&lt;&#x2F;em&gt; one of its files has a settled DB row. &lt;strong&gt;Ground truth beats heuristics; the max-date guess lied twice, the inbox count never did.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Anonymous calls&lt;&#x2F;strong&gt; — files from a withheld number didn’t match the filename pattern and went invisible to day-grouping. Parser widened, rows repaired.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A duplicate bug&lt;&#x2F;strong&gt; — identical audio under a new name was marked &lt;code&gt;done&lt;&#x2F;code&gt; with no transcript (one hash shared by 103 empty ring-out files) and polluted the day dumps. Now duplicates inherit their twin’s status and content.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reaped background processes&lt;&#x2F;strong&gt; — &lt;code&gt;nohup&lt;&#x2F;code&gt; jobs were harvested by the environment and died unnoticed for hours. Fix: all long-running work as harness-managed background tasks, with the watchdog as backstop.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Self-healing has to be layered:&lt;&#x2F;strong&gt; process-level (supervisor: crash &#x2F; hang &#x2F; poison &#x2F; orphan) &lt;em&gt;and&lt;&#x2F;em&gt; system-level (watchdog: supervisor dead? web down? worker gone?).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-numbers-as-of-this-writing&quot;&gt;The numbers (as of this writing)&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Corpus&lt;&#x2F;td&gt;&lt;td&gt;~33,000 mp3, ~570 audio hours, 13 months&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Processed&lt;&#x2F;td&gt;&lt;td&gt;~6,100 files (19%), from both ends&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Summaries&lt;&#x2F;td&gt;&lt;td&gt;60 daily + 7 weekly&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Local speed&lt;&#x2F;td&gt;&lt;td&gt;dual-model ~0.2 RTF; ~430 files&#x2F;hour&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Day-agent cost&lt;&#x2F;td&gt;&lt;td&gt;~50k–450k subagent tokens&#x2F;day (avg ~200k)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Diarization&lt;&#x2F;td&gt;&lt;td&gt;2-speaker prior + context repair rescued the 79% of long calls that collapsed to one voice&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;how-this-maps-to-a-customer-service-function&quot;&gt;How this maps to a customer-service function&lt;&#x2F;h2&gt;
&lt;p&gt;The original use case is personal call intelligence, but the exact same pipeline is a &lt;strong&gt;customer-service&lt;&#x2F;strong&gt; system:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two-track ASR + LLM fusion&lt;&#x2F;strong&gt; → accurate Danish transcripts of support calls.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Per-call &#x2F; per-day &#x2F; per-week summaries&lt;&#x2F;strong&gt; → QA without listening to every call.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The commitments table&lt;&#x2F;strong&gt; generalises directly to &lt;strong&gt;SLA &#x2F; promise tracking&lt;&#x2F;strong&gt; — what an agent promised a customer, and whether it was delivered, carried week to week.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The shared name register&lt;&#x2F;strong&gt; → a consistent customer&#x2F;contact directory built from the calls themselves.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The patterns view&lt;&#x2F;strong&gt; (volume by hour, call-direction asymmetry, recurring topics) → staffing and escalation insight, all from metadata + summaries.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PII discipline&lt;&#x2F;strong&gt; is built in: the “never invent, flag uncertainty” rule, and redaction before anything is stored.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And the privacy story is the selling point: everything runs &lt;strong&gt;locally behind Tailscale&lt;&#x2F;strong&gt; — audio never leaves the building, you set retention and redaction, and there’s no public endpoint. (The honest boundary: if your language layer is a cloud LLM API rather than a local harness, that text leaves — so redact first or keep the LLM local too.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-lessons-distilled&quot;&gt;The lessons, distilled&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Analyse the corpses first&lt;&#x2F;strong&gt; — nine dead experiments held almost every right idea. Synthesis beats invention.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Metadata is gold&lt;&#x2F;strong&gt; — filenames alone gave speaker identity, day structure and the whole pattern analysis before a second of audio was decoded.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Two mediocre, diverse ASR tracks + a model that fuses them beat one good track.&lt;&#x2F;strong&gt; Error diversity is the point.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SOTA is task-dependent&lt;&#x2F;strong&gt; — neural denoise &lt;em&gt;hurt&lt;&#x2F;em&gt; ASR measurably; a 50-year-old band-pass helped.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency is the freedom to fail&lt;&#x2F;strong&gt; — hash + status made every crash, restart and mid-run migration harmless.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Self-healing in layers&lt;&#x2F;strong&gt; — supervisor for the process, watchdog for the system.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Ground truth over heuristics&lt;&#x2F;strong&gt; for progress gates — count the inbox, don’t guess from the max date.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;An LLM can be the pipeline’s expensive step without an API&lt;&#x2F;strong&gt; — when the harness itself is the model, a “summary stage” is a subagent with a good instruction file, a shared register, and an honesty norm.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Factory reset a Yealink phone — even without the admin password (2026)</title>
        <published>2026-06-10T00:00:00+00:00</published>
        <updated>2026-06-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/factory-reset-yealink-phone/"/>
        <id>https://www.x2q.net/post/factory-reset-yealink-phone/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/factory-reset-yealink-phone/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; To factory reset a &lt;strong&gt;Yealink&lt;&#x2F;strong&gt; SIP desk phone you have three options: the &lt;strong&gt;keypad menu&lt;&#x2F;strong&gt; (needs the admin password), the &lt;strong&gt;web UI&lt;&#x2F;strong&gt; (needs the admin password), or a &lt;strong&gt;hardware key-hold&lt;&#x2F;strong&gt; that needs &lt;strong&gt;no password at all&lt;&#x2F;strong&gt; — hold the &lt;strong&gt;OK&lt;&#x2F;strong&gt; key for ~10 seconds. The last one is what you want for a used or inherited phone with an unknown admin login. This also covers the 2026 default-password situation: old firmware is &lt;code&gt;admin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;admin&lt;&#x2F;code&gt;, newer units ship a &lt;strong&gt;unique password on a label&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-default-password-then-now&quot;&gt;The default password, then → now&lt;&#x2F;h2&gt;
&lt;p&gt;This trips people up, so get it straight first:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Older firmware:&lt;&#x2F;strong&gt; username &lt;code&gt;admin&lt;&#x2F;code&gt;, password &lt;code&gt;admin&lt;&#x2F;code&gt;. The classic default.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Newer firmware (security hardening, ~2021 onward):&lt;&#x2F;strong&gt; Yealink ships each phone with a &lt;strong&gt;unique random default password&lt;&#x2F;strong&gt; printed on a sticker (usually on the underside or the box), or forces you to set a new password on first web login. So &lt;code&gt;admin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;admin&lt;&#x2F;code&gt; &lt;strong&gt;will not work&lt;&#x2F;strong&gt; on recent units.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you have the password, reset from the menu or web UI. If you don’t, skip to the &lt;strong&gt;hardware reset&lt;&#x2F;strong&gt; — it clears the password entirely.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;method-1-hardware-reset-no-admin-password-needed&quot;&gt;Method 1 — Hardware reset (no admin password needed)&lt;&#x2F;h2&gt;
&lt;p&gt;This is the one most people are searching for. With the phone &lt;strong&gt;powered on and idle&lt;&#x2F;strong&gt; (not in a call, not in a menu):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Press and &lt;strong&gt;hold the OK key&lt;&#x2F;strong&gt; — the round&#x2F;centre key below the screen — for about &lt;strong&gt;10 seconds&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;The screen shows &lt;strong&gt;“Reset to factory settings?”&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Press &lt;strong&gt;OK&lt;&#x2F;strong&gt; to confirm.&lt;&#x2F;li&gt;
&lt;li&gt;The phone wipes everything and reboots. This takes a minute or two; don’t unplug it.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;No login required — this resets the admin password along with everything else. (On a few models the prompt is triggered by holding the &lt;strong&gt;X&lt;&#x2F;strong&gt;&#x2F;cancel key instead; if OK doesn’t do it, try that.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;method-2-from-the-phone-s-keypad-needs-admin-password&quot;&gt;Method 2 — From the phone’s keypad (needs admin password)&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Press the &lt;strong&gt;Menu&lt;&#x2F;strong&gt; soft key.&lt;&#x2F;li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Advanced Settings&lt;&#x2F;strong&gt; and enter the admin password (&lt;code&gt;admin&lt;&#x2F;code&gt; on old firmware, or the label password).&lt;&#x2F;li&gt;
&lt;li&gt;Choose &lt;strong&gt;Reset to Factory&lt;&#x2F;strong&gt; (sometimes &lt;strong&gt;Reset Config&lt;&#x2F;strong&gt;) and confirm.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;method-3-from-the-web-interface-needs-admin-password&quot;&gt;Method 3 — From the web interface (needs admin password)&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Find the phone’s IP: &lt;strong&gt;Menu → Status&lt;&#x2F;strong&gt; (or &lt;strong&gt;Settings → Status&lt;&#x2F;strong&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;Browse to &lt;code&gt;http:&#x2F;&#x2F;&amp;lt;phone-ip&amp;gt;&#x2F;&lt;&#x2F;code&gt; and log in as &lt;code&gt;admin&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Upgrade&lt;&#x2F;strong&gt; (or &lt;strong&gt;Reset &amp;amp; Reboot&lt;&#x2F;strong&gt;) and click &lt;strong&gt;Reset to Factory&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;after-the-reset&quot;&gt;After the reset&lt;&#x2F;h2&gt;
&lt;p&gt;A full reset erases the &lt;strong&gt;SIP account, network config, contacts and call history&lt;&#x2F;strong&gt; — the phone comes back as if new. Before you reset (if you can log in), note your &lt;strong&gt;SIP credentials&lt;&#x2F;strong&gt; (server&#x2F;registrar, username, auth ID, password) so you can re-register it afterward. On newer firmware you’ll be asked to set a fresh admin password on first login again.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No admin password?&lt;&#x2F;strong&gt; Hold the &lt;strong&gt;OK&lt;&#x2F;strong&gt; key ~10 s while idle → confirm → done. No login needed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Have the password?&lt;&#x2F;strong&gt; Reset from &lt;strong&gt;Menu → Settings → Advanced&lt;&#x2F;strong&gt;, or the &lt;strong&gt;web UI&lt;&#x2F;strong&gt; at &lt;code&gt;http:&#x2F;&#x2F;&amp;lt;phone-ip&amp;gt;&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Default password:&lt;&#x2F;strong&gt; &lt;code&gt;admin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;admin&lt;&#x2F;code&gt; on old firmware; a &lt;strong&gt;unique label password&lt;&#x2F;strong&gt; on newer units.&lt;&#x2F;li&gt;
&lt;li&gt;A factory reset &lt;strong&gt;erases the SIP account and contacts&lt;&#x2F;strong&gt; — save your SIP credentials first.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Update Garmin firmware and maps in 2026 — from WebUpdater to Garmin Express</title>
        <published>2026-06-10T00:00:00+00:00</published>
        <updated>2026-06-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/update-garmin-firmware-and-maps/"/>
        <id>https://www.x2q.net/post/update-garmin-firmware-and-maps/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/update-garmin-firmware-and-maps/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; If you searched for &lt;em&gt;Garmin WebUpdater&lt;&#x2F;em&gt; or &lt;em&gt;how to update Garmin firmware&lt;&#x2F;em&gt;, the tool you remember is gone. &lt;strong&gt;WebUpdater was discontinued (~2015)&lt;&#x2F;strong&gt;; the current path is &lt;strong&gt;Garmin Express&lt;&#x2F;strong&gt; (a desktop app, USB cable) for nüvi&#x2F;DriveSmart sat-navs, or &lt;strong&gt;Garmin Connect&lt;&#x2F;strong&gt; (phone, over the air) for watches and newer handhelds. This post covers the 2026 way to update &lt;strong&gt;firmware and maps&lt;&#x2F;strong&gt; on each, what to do with an old nüvi, and the hidden &lt;strong&gt;nüvi debug menu&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;then-now-webupdater-is-dead&quot;&gt;Then → now: WebUpdater is dead&lt;&#x2F;h2&gt;
&lt;p&gt;For years you updated a Garmin with &lt;strong&gt;WebUpdater&lt;&#x2F;strong&gt; — a small desktop helper that checked for firmware. Garmin retired it and folded everything into two apps:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Garmin Express&lt;&#x2F;strong&gt; — desktop (Windows&#x2F;macOS), connects over &lt;strong&gt;USB cable&lt;&#x2F;strong&gt;. This is what replaced WebUpdater for sat-navs (nüvi, DriveSmart, Drive) and is also the cabled path for watches&#x2F;handhelds and &lt;strong&gt;map&lt;&#x2F;strong&gt; updates.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Garmin Connect&lt;&#x2F;strong&gt; — phone app (iOS&#x2F;Android), updates &lt;strong&gt;over the air&lt;&#x2F;strong&gt; over Bluetooth&#x2F;Wi-Fi. This is the path for watches (Forerunner, fēnix, Venu), bike computers (Edge), and modern handhelds.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you still have WebUpdater installed, uninstall it — it no longer does anything useful.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;update-a-nuvi-drivesmart-sat-nav-firmware-maps&quot;&gt;Update a nüvi &#x2F; DriveSmart sat-nav (firmware + maps)&lt;&#x2F;h2&gt;
&lt;p&gt;This is the case behind most “update Garmin firmware” searches.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Download &lt;strong&gt;Garmin Express&lt;&#x2F;strong&gt; from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.garmin.com&#x2F;en-US&#x2F;software&#x2F;express&#x2F;&quot;&gt;garmin.com&#x2F;express&lt;&#x2F;a&gt; and install it on a Windows or Mac computer.&lt;&#x2F;li&gt;
&lt;li&gt;Connect the device with a &lt;strong&gt;USB cable&lt;&#x2F;strong&gt; and power it on. Wait for the computer to recognise it as a drive.&lt;&#x2F;li&gt;
&lt;li&gt;Open Garmin Express. It detects the unit and lists available &lt;strong&gt;software (firmware)&lt;&#x2F;strong&gt; and &lt;strong&gt;map&lt;&#x2F;strong&gt; updates.&lt;&#x2F;li&gt;
&lt;li&gt;Click &lt;strong&gt;Install All&lt;&#x2F;strong&gt;, or pick individual updates. Map files are large (often several GB) — leave it plugged in and don’t disconnect mid-update.&lt;&#x2F;li&gt;
&lt;li&gt;When it finishes, safely eject and unplug. The unit reboots into the new firmware.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;&lt;strong&gt;Map updates&lt;&#x2F;strong&gt; appear here too. If your unit has &lt;strong&gt;lifetime maps&lt;&#x2F;strong&gt; (“LM”&#x2F;“LMT” in the model name), updates are free for the supported life of the device. Otherwise Express will offer a paid map purchase.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;update-a-watch-edge-newer-handheld-over-the-air&quot;&gt;Update a watch &#x2F; Edge &#x2F; newer handheld (over the air)&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;strong&gt;Garmin Connect&lt;&#x2F;strong&gt; on your phone and pair the device.&lt;&#x2F;li&gt;
&lt;li&gt;Connect updates download automatically; you’ll get a prompt when firmware is ready.&lt;&#x2F;li&gt;
&lt;li&gt;Keep the device charged and near the phone — it installs and reboots on its own.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;For a cabled update of these devices (or if OTA fails), Garmin Express on a computer works too.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;very-old-nuvi-units-what-still-works&quot;&gt;Very old nüvi units — what still works&lt;&#x2F;h2&gt;
&lt;p&gt;Many early nüvi models (the 2007–2012 era) are now &lt;strong&gt;end-of-life&lt;&#x2F;strong&gt;: firmware may still install via Garmin Express, but &lt;strong&gt;map updates are no longer offered&lt;&#x2F;strong&gt; and lifetime-map coverage has ended. If Express shows “up to date” with an old map year, that’s usually the end of the road — the hardware simply isn’t supported with new map data anymore. The unit keeps working with the maps it has.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bonus-the-hidden-nuvi-debug-menu&quot;&gt;Bonus: the hidden nüvi debug menu&lt;&#x2F;h2&gt;
&lt;p&gt;A long-standing trick for diagnosing a flaky nüvi: power it on, then &lt;strong&gt;press and hold the bottom-right corner of the touchscreen&lt;&#x2F;strong&gt; for a few seconds. A &lt;strong&gt;diagnostics screen&lt;&#x2F;strong&gt; appears — software version, touchscreen calibration test, GPS signal test, battery info. It’s safe to view; avoid changing touchscreen calibration unless the screen is genuinely misaligned.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WebUpdater is discontinued&lt;&#x2F;strong&gt; — use &lt;strong&gt;Garmin Express&lt;&#x2F;strong&gt; (desktop, USB) or &lt;strong&gt;Garmin Connect&lt;&#x2F;strong&gt; (phone, over the air).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;nüvi &#x2F; DriveSmart&lt;&#x2F;strong&gt;: Garmin Express over USB handles both firmware and maps; “Install All”.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Watches &#x2F; Edge &#x2F; handhelds&lt;&#x2F;strong&gt;: Garmin Connect updates over the air.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Lifetime maps&lt;&#x2F;strong&gt; (LM&#x2F;LMT models) update free; very old nüvi units are end-of-life for new map data.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;nüvi debug menu&lt;&#x2F;strong&gt;: hold the bottom-right corner of the screen after power-on.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>From Launchy to Raycast: the history of the keystroke launcher (2007 → 2026)</title>
        <published>2026-06-07T00:00:00+00:00</published>
        <updated>2026-06-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/history-of-application-launchers-launchy-to-raycast/"/>
        <id>https://www.x2q.net/post/history-of-application-launchers-launchy-to-raycast/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/history-of-application-launchers-launchy-to-raycast/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; In 2007 I posted a short screencast of &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;LBW1L2YOKks&quot;&gt;Launchy&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; — a “keystroke application launcher” that let you summon any program on Windows by hitting a hotkey and typing a few letters, instead of hunting through the Start menu. That pattern — &lt;strong&gt;hotkey → type → fuzzy-match → launch&lt;&#x2F;strong&gt; — was born on the Mac (Quicksilver, LaunchBar), crossed to Windows with Launchy, spread to Linux, and then did two things at once: it matured into full productivity platforms (&lt;strong&gt;Alfred&lt;&#x2F;strong&gt;, then &lt;strong&gt;Raycast&lt;&#x2F;strong&gt;), and it dissolved into the &lt;strong&gt;Cmd+K command palette&lt;&#x2F;strong&gt; that now lives inside nearly every app you use. Here’s the 19-year arc.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A companion to the other time-capsule posts here — like &lt;a href=&quot;&#x2F;post&#x2F;linux-gui-apps-on-windows-xming-to-wslg&#x2F;&quot;&gt;Xming on Windows XP → WSLg&lt;&#x2F;a&gt; and &lt;a href=&quot;&#x2F;post&#x2F;hack-wireless-network&#x2F;&quot;&gt;25 years of Wi-Fi security&lt;&#x2F;a&gt;. An old screencast, and what the idea grew into.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-setup-back-then&quot;&gt;The setup, back then&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;LBW1L2YOKks&quot;&gt;video&lt;&#x2F;a&gt; (“Shiny Windows Application Launcher” — &lt;em&gt;Shiny&lt;&#x2F;em&gt; was a Launchy skin) shows the 2007 workflow. You install &lt;strong&gt;Launchy&lt;&#x2F;strong&gt;, it indexes your Start menu, and from then on:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;Alt+Space&lt;&#x2F;strong&gt;. A small translucent box appears in the middle of the screen.&lt;&#x2F;li&gt;
&lt;li&gt;Type a few letters — &lt;code&gt;fir&lt;&#x2F;code&gt; for Firefox, &lt;code&gt;wor&lt;&#x2F;code&gt; for Word.&lt;&#x2F;li&gt;
&lt;li&gt;Launchy fuzzy-matches against everything it indexed and shows the best hit.&lt;&#x2F;li&gt;
&lt;li&gt;Hit &lt;strong&gt;Enter&lt;&#x2F;strong&gt;. The app launches. The box vanishes.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;No mouse, no Start menu, no desktop icons. After a week your fingers knew the three-letter prefix for every app you used. Launchy — written by Josh Karlin and open-sourced — was explicitly pitched as &lt;strong&gt;“Quicksilver for Windows”&lt;&#x2F;strong&gt;, and that comparison is the key to the whole story.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-the-idea-actually-came-from&quot;&gt;Where the idea actually came from&lt;&#x2F;h2&gt;
&lt;p&gt;Launchy didn’t invent the keystroke launcher — it brought a &lt;strong&gt;Mac&lt;&#x2F;strong&gt; idea to Windows:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LaunchBar&lt;&#x2F;strong&gt; (Objective Development) — the granddaddy, with roots on &lt;strong&gt;NeXTSTEP in the 1990s&lt;&#x2F;strong&gt;, reborn on Mac OS X. Type an abbreviation, get the thing.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Quicksilver&lt;&#x2F;strong&gt; (Nicholas Jitkoff &#x2F; Blacktree, ~2003) — the legendary, almost mystical Mac launcher. Not just “launch an app” but a verb-noun grammar: select a &lt;em&gt;thing&lt;&#x2F;em&gt;, choose an &lt;em&gt;action&lt;&#x2F;em&gt;, pick a &lt;em&gt;target&lt;&#x2F;em&gt;. It defined the genre and inspired a generation (it was open-sourced in 2007 when its creator left for Google).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Spotlight&lt;&#x2F;strong&gt; (Mac OS X Tiger, 2005) — Apple baked search into the OS, which is where most casual users met the “just type to find things” idea.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Launchy (2004→) was the Windows answer to all that, and the 2007 video is a snapshot of it in its prime.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;at-a-glance-19-years-of-launchers&quot;&gt;At a glance — 19 years of launchers&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Era&lt;&#x2F;th&gt;&lt;th&gt;macOS&lt;&#x2F;th&gt;&lt;th&gt;Windows&lt;&#x2F;th&gt;&lt;th&gt;Linux&lt;&#x2F;th&gt;&lt;th&gt;The pattern&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2003–05&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Quicksilver, LaunchBar, Spotlight&lt;&#x2F;td&gt;&lt;td&gt;Start menu (Vista adds search, 2007)&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;Launcher is a Mac power-user thing&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2007 (the video)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Quicksilver peak&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Launchy&lt;&#x2F;strong&gt; (“Quicksilver for Windows”)&lt;&#x2F;td&gt;&lt;td&gt;—&lt;&#x2F;td&gt;&lt;td&gt;Keystroke launcher crosses to Windows&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2008–10&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Quicksilver stalls → &lt;strong&gt;Alfred&lt;&#x2F;strong&gt; (2010)&lt;&#x2F;td&gt;&lt;td&gt;Launchy peaks, then slows&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;GNOME Do&lt;&#x2F;strong&gt;, Katapult&lt;&#x2F;td&gt;&lt;td&gt;Launcher goes mainstream-power-user&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2011–15&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Alfred + Powerpack dominates&lt;&#x2F;td&gt;&lt;td&gt;Wox (2014)&lt;&#x2F;td&gt;&lt;td&gt;Synapse, Albert&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Command palette&lt;&#x2F;strong&gt; appears (Sublime, then VS Code)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2016–20&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Raycast&lt;&#x2F;strong&gt; founded (2020)&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;PowerToys Run&lt;&#x2F;strong&gt; (2020), Flow Launcher&lt;&#x2F;td&gt;&lt;td&gt;Rofi, Ulauncher&lt;&#x2F;td&gt;&lt;td&gt;Cmd+K spreads into every web app&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2026 (today)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Raycast dominant; Spotlight + Apple Intelligence&lt;&#x2F;td&gt;&lt;td&gt;PowerToys &lt;strong&gt;Command Palette&lt;&#x2F;strong&gt;; Raycast (beta)&lt;&#x2F;td&gt;&lt;td&gt;Rofi &#x2F; Ulauncher &#x2F; Albert&lt;&#x2F;td&gt;&lt;td&gt;Launcher = AI-powered command hub&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;1. The launcher became a platform.&lt;&#x2F;strong&gt; Launchy launched apps and files. &lt;strong&gt;Alfred&lt;&#x2F;strong&gt; (2010) added &lt;em&gt;workflows&lt;&#x2F;em&gt; — chain actions, run scripts, clipboard history, snippets — and became the Mac standard for a decade. Then &lt;strong&gt;Raycast&lt;&#x2F;strong&gt; (2020) rebuilt the idea as a native, extensible platform: an extension store, window management, clipboard history, snippets, calculators, and now built-in &lt;strong&gt;AI&lt;&#x2F;strong&gt;. The humble launch box turned into the place power users run half their day. Raycast’s &lt;strong&gt;Windows beta arrived in late 2025&lt;&#x2F;strong&gt;, closing the loop back to where Launchy started.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. Windows kept reinventing it.&lt;&#x2F;strong&gt; Launchy faded after ~2010. &lt;strong&gt;Wox&lt;&#x2F;strong&gt; (open source, 2014) carried the torch and spawned &lt;strong&gt;Flow Launcher&lt;&#x2F;strong&gt; (a Wox fork with a plugin marketplace and AI plugins). Microsoft itself shipped &lt;strong&gt;PowerToys Run&lt;&#x2F;strong&gt; (2020, &lt;code&gt;Alt+Space&lt;&#x2F;code&gt; — a knowing nod to Launchy), now succeeded by the &lt;strong&gt;PowerToys Command Palette&lt;&#x2F;strong&gt;. The pattern is so obviously good that the OS vendor adopted it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. Linux always had its own.&lt;&#x2F;strong&gt; &lt;strong&gt;GNOME Do&lt;&#x2F;strong&gt; (2008) was the early Quicksilver-alike; today it’s &lt;strong&gt;Rofi&lt;&#x2F;strong&gt; and &lt;strong&gt;dmenu&lt;&#x2F;strong&gt; (minimal, scriptable, beloved by tiling-WM users), &lt;strong&gt;Ulauncher&lt;&#x2F;strong&gt;, and &lt;strong&gt;Albert&lt;&#x2F;strong&gt;. Same muscle memory, different ecosystem.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4. The biggest shift: the launcher dissolved into every app.&lt;&#x2F;strong&gt; The defining move of the last decade isn’t a better launcher — it’s that the &lt;em&gt;command palette&lt;&#x2F;em&gt; became a universal UX primitive. &lt;strong&gt;Sublime Text&lt;&#x2F;strong&gt; (~2011) and then &lt;strong&gt;VS Code&lt;&#x2F;strong&gt; (&lt;code&gt;Ctrl&#x2F;Cmd+Shift+P&lt;&#x2F;code&gt;) made “hit a hotkey, type a command” the way you drive an editor. Then &lt;strong&gt;Cmd+K&lt;&#x2F;strong&gt; showed up everywhere: Slack, Linear, Notion, GitHub, Figma, Vercel, Stripe, your browser. The keystroke launcher stopped being a separate app you install and became a control built into the software itself. On the terminal, &lt;strong&gt;fzf&lt;&#x2F;strong&gt; (2013) is the same idea distilled to one fuzzy-finder binary.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;5. Then AI ate the command bar.&lt;&#x2F;strong&gt; The newest turn: the box you type into now talks to a model. &lt;strong&gt;Raycast AI&lt;&#x2F;strong&gt;, Flow Launcher’s AI plugins, &lt;strong&gt;Spotlight + Apple Intelligence&lt;&#x2F;strong&gt;, Windows &lt;strong&gt;Copilot&lt;&#x2F;strong&gt;. “Hotkey → type → fuzzy-match → act” is becoming “hotkey → ask → the launcher figures out the action.” Launchy’s little translucent box was the larval form of today’s AI command bar.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-to-use-today&quot;&gt;What to use today&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;macOS&lt;&#x2F;strong&gt; — &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.raycast.com&#x2F;&quot;&gt;Raycast&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; is the modern default: launching, clipboard history, window management, snippets, an extension store, and AI in one hotkey. &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.alfredapp.com&#x2F;&quot;&gt;Alfred&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; is the alternative if you prefer a one-time purchase and local automation. Spotlight is built in and now quite good.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;&#x2F;strong&gt; — &lt;strong&gt;PowerToys Command Palette&lt;&#x2F;strong&gt; (official, free, ships in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;en-us&#x2F;windows&#x2F;powertoys&#x2F;&quot;&gt;PowerToys&lt;&#x2F;a&gt;), &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.flowlauncher.com&#x2F;&quot;&gt;Flow Launcher&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; (open source, plugin marketplace), or &lt;strong&gt;Raycast&lt;&#x2F;strong&gt; (Windows beta since late 2025). Pair any of them with &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.voidtools.com&#x2F;&quot;&gt;Everything&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; for instant file search.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;&#x2F;strong&gt; — &lt;strong&gt;Rofi&lt;&#x2F;strong&gt; or &lt;strong&gt;dmenu&lt;&#x2F;strong&gt; for the scriptable&#x2F;tiling crowd, &lt;strong&gt;Ulauncher&lt;&#x2F;strong&gt; or &lt;strong&gt;Albert&lt;&#x2F;strong&gt; for a friendlier GUI.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Terminal&lt;&#x2F;strong&gt; — &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;junegunn&#x2F;fzf&quot;&gt;fzf&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;. Pipe anything into it, fuzzy-find, act. The launcher idea in 30 lines of muscle memory.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;The 2007 Launchy clip looks quaint — a translucent box, a Shiny skin, three letters and Enter. But the idea in it won completely. It started as a Mac curiosity, Launchy brought it to Windows, Linux picked it up, and from there it went two directions at once: &lt;em&gt;up&lt;&#x2F;em&gt; into productivity platforms like Raycast, and &lt;em&gt;sideways&lt;&#x2F;em&gt; into every app as the Cmd+K command palette. Today you use that 2007 idea dozens of times a day — you just don’t install it anymore, because it’s everywhere. And the box is starting to think.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;what-was-launchy&quot;&gt;What was Launchy?&lt;&#x2F;h3&gt;
&lt;p&gt;A free, open-source keystroke launcher for Windows (later cross-platform), first released by Josh Karlin around 2004. Hotkey, type a few letters, fuzzy-match, launch — “Quicksilver for Windows”. The 2007 video shows it on the Shiny skin.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-launchy-still-maintained&quot;&gt;Is Launchy still maintained?&lt;&#x2F;h3&gt;
&lt;p&gt;No — development tapered off around 2010 and it’s effectively dormant. On Windows today use PowerToys Command Palette, Flow Launcher, or Raycast.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;app-launcher-vs-command-palette-what-s-the-difference&quot;&gt;App launcher vs command palette — what’s the difference?&lt;&#x2F;h3&gt;
&lt;p&gt;Same pattern, different scope. A launcher (Quicksilver, Launchy, Alfred, Raycast) is system-wide; a command palette (VS Code’s &lt;code&gt;Ctrl+Shift+P&lt;&#x2F;code&gt;, the &lt;code&gt;Cmd+K&lt;&#x2F;code&gt; bar in Slack&#x2F;Linear&#x2F;GitHub) is that same UX inside one app. The launcher came first; the palette is it absorbed into everything.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-should-i-use-in-2026&quot;&gt;What should I use in 2026?&lt;&#x2F;h3&gt;
&lt;p&gt;macOS: Raycast or Alfred. Windows: PowerToys Command Palette, Flow Launcher, or Raycast. Linux: Rofi, Ulauncher, or Albert. Terminal: fzf.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;The keystroke launcher — &lt;strong&gt;hotkey → type → fuzzy-match → launch&lt;&#x2F;strong&gt; — started on the Mac (&lt;strong&gt;LaunchBar&lt;&#x2F;strong&gt;, &lt;strong&gt;Quicksilver&lt;&#x2F;strong&gt;) and reached Windows via &lt;strong&gt;Launchy&lt;&#x2F;strong&gt;, which my &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;LBW1L2YOKks&quot;&gt;2007 video&lt;&#x2F;a&gt; demos.&lt;&#x2F;li&gt;
&lt;li&gt;It grew &lt;em&gt;up&lt;&#x2F;em&gt; into platforms: &lt;strong&gt;Alfred&lt;&#x2F;strong&gt; (2010) → &lt;strong&gt;Raycast&lt;&#x2F;strong&gt; (2020), now AI-powered, with a &lt;strong&gt;Windows beta&lt;&#x2F;strong&gt; as of late 2025.&lt;&#x2F;li&gt;
&lt;li&gt;Windows kept reinventing it: &lt;strong&gt;Wox&lt;&#x2F;strong&gt; → &lt;strong&gt;Flow Launcher&lt;&#x2F;strong&gt;, and Microsoft’s &lt;strong&gt;PowerToys Run → Command Palette&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;It also spread &lt;em&gt;sideways&lt;&#x2F;em&gt; into every app as the &lt;strong&gt;Cmd+K command palette&lt;&#x2F;strong&gt; (Sublime&#x2F;VS Code → Slack, Linear, Notion, GitHub), and into the terminal as &lt;strong&gt;fzf&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;The 2007 idea is now everywhere — and increasingly an AI command bar.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>From Xming on Windows XP to WSLg: 18 years of Linux GUI apps on Windows (2026)</title>
        <published>2026-06-07T00:00:00+00:00</published>
        <updated>2026-06-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/linux-gui-apps-on-windows-xming-to-wslg/"/>
        <id>https://www.x2q.net/post/linux-gui-apps-on-windows-xming-to-wslg/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/linux-gui-apps-on-windows-xming-to-wslg/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; Running a Linux GUI app on a Windows machine used to mean installing an &lt;strong&gt;X server&lt;&#x2F;strong&gt; (Xming, later VcXsrv), pointing an SSH session at it with &lt;code&gt;ssh -X&lt;&#x2F;code&gt;, and watching a remote &lt;code&gt;gedit&lt;&#x2F;code&gt; window paint itself onto your Windows desktop. I have &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=P3hHsfdFusw&quot;&gt;an old screen recording of exactly that&lt;&#x2F;a&gt; — Xming putting gedit on &lt;strong&gt;Windows XP&lt;&#x2F;strong&gt;. Eighteen years later you mostly don’t bother: &lt;strong&gt;WSLg&lt;&#x2F;strong&gt; ships a Wayland + X server &lt;em&gt;inside&lt;&#x2F;em&gt; Windows, so a Linux app installed in WSL2 just opens like any other window. This post traces the arc — X11 forwarding → VcXsrv → Wayland → WSLg — and what to actually use in 2026.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a companion to the other “then vs now” notes on this blog — like &lt;a href=&quot;&#x2F;post&#x2F;local-speech-to-text-whisper-cpp&#x2F;&quot;&gt;local speech-to-text replacing the cloud Speech API&lt;&#x2F;a&gt; and &lt;a href=&quot;&#x2F;post&#x2F;hack-wireless-network&#x2F;&quot;&gt;25 years of Wi-Fi security&lt;&#x2F;a&gt;. Same shape: a thing that was fiddly in the XP era is now built in.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-setup-back-then&quot;&gt;The setup, back then&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=P3hHsfdFusw&quot;&gt;video&lt;&#x2F;a&gt; shows the canonical mid-2000s trick. The pieces:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xming&lt;&#x2F;strong&gt; — a free X11 server for Windows (a port of the X.Org server). It ran in the Windows system tray and provided a &lt;em&gt;display&lt;&#x2F;em&gt; that X clients could draw to.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;An SSH client&lt;&#x2F;strong&gt; — usually PuTTY, with “Enable X11 forwarding” ticked, or &lt;code&gt;ssh -X&lt;&#x2F;code&gt; from Cygwin.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A remote Linux box&lt;&#x2F;strong&gt; running the actual app (&lt;code&gt;gedit&lt;&#x2F;code&gt;, an editor) with no local window of its own.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The flow: SSH connects to the Linux host, the host sets &lt;code&gt;$DISPLAY&lt;&#x2F;code&gt; to tunnel X11 back through the encrypted SSH channel, the app draws to Xming, and a Linux window appears on Windows XP. The application runs entirely on the remote machine; only the &lt;em&gt;pixels and input events&lt;&#x2F;em&gt; travel. This worked because &lt;strong&gt;X11 was network-transparent from day one&lt;&#x2F;strong&gt; — a 1984 design decision that aged into a genuinely useful feature.&lt;&#x2F;p&gt;
&lt;p&gt;It was clever, and it was also slow and brittle: every menu and redraw was a round-trip, so over anything but a LAN it felt like wading through treacle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;at-a-glance-18-years-of-linux-gui-on-windows&quot;&gt;At a glance — 18 years of Linux GUI on Windows&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Era&lt;&#x2F;th&gt;&lt;th&gt;How you did it&lt;&#x2F;th&gt;&lt;th&gt;What ran the display&lt;&#x2F;th&gt;&lt;th&gt;Pain points&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2007 (XP)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ssh -X&lt;&#x2F;code&gt; &#x2F; PuTTY → Xming&lt;&#x2F;td&gt;&lt;td&gt;Xming X server in the tray&lt;&#x2F;td&gt;&lt;td&gt;Manual install, chatty X11, laggy over WAN&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2012&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Same, or X over VNC&lt;&#x2F;td&gt;&lt;td&gt;Xming &#x2F; Xming-mesa, VcXsrv arrives (2011)&lt;&#x2F;td&gt;&lt;td&gt;X server still a separate moving part&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2017&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;VcXsrv + WSL1&lt;&#x2F;td&gt;&lt;td&gt;VcXsrv, set &lt;code&gt;DISPLAY=localhost:0&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;WSL1 quirks, fonts&#x2F;clipboard glitches&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;~2021&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;WSL2 + manual X server, &lt;em&gt;or&lt;&#x2F;em&gt; WSLg preview&lt;&#x2F;td&gt;&lt;td&gt;VcXsrv &#x2F; X410, then WSLg&lt;&#x2F;td&gt;&lt;td&gt;Transitional; manual &lt;code&gt;DISPLAY&lt;&#x2F;code&gt; export still common&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2026 (today)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;WSLg, built in&lt;&#x2F;td&gt;&lt;td&gt;Weston (Wayland) + XWayland, auto&lt;&#x2F;td&gt;&lt;td&gt;Basically none — &lt;code&gt;apt install&lt;&#x2F;code&gt;, app opens&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;what-actually-changed&quot;&gt;What actually changed&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;1. The X server stopped being yours to manage.&lt;&#x2F;strong&gt; For ~25 years the X server was a thing &lt;em&gt;you&lt;&#x2F;em&gt; installed and babysat on Windows. With &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;microsoft&#x2F;wslg&quot;&gt;WSLg&lt;&#x2F;a&gt; — announced at Microsoft Build 2021, now standard on Windows 11 and Windows 10 21H2+ — Microsoft bundles a whole display stack into a system distro: a &lt;strong&gt;Weston&lt;&#x2F;strong&gt; Wayland compositor, &lt;strong&gt;XWayland&lt;&#x2F;strong&gt; for legacy X11 apps, a PulseAudio server for sound, and an RDP link that paints the windows onto the Windows desktop. You install a Linux GUI app in WSL2 and it opens. No &lt;code&gt;DISPLAY&lt;&#x2F;code&gt;, no tray icon, no PuTTY checkbox.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;# Inside WSL2 on Windows 11 — no X server to install:
sudo apt update &amp;amp;&amp;amp; sudo apt install -y gedit
gedit            # a real Linux gedit window opens on Windows
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;2. Wayland replaced X11 on the Linux side.&lt;&#x2F;strong&gt; The protocol that made the original trick possible is on its way out. Modern Linux desktops default to &lt;strong&gt;Wayland&lt;&#x2F;strong&gt;, which — unlike X11 — is &lt;em&gt;not&lt;&#x2F;em&gt; network-transparent. So the “just forward it over SSH” reflex doesn’t map cleanly anymore; the Wayland-native answer for remote display is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gitlab.freedesktop.org&#x2F;mstoeckl&#x2F;waypipe&quot;&gt;&lt;code&gt;waypipe&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, and XWayland keeps old X11 apps working in the meantime.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. Xming itself faded into a paid niche.&lt;&#x2F;strong&gt; Xming is still alive — Colin Harrison shipped 7.7.x builds as recently as January 2026 — but the freely downloadable public release froze at &lt;strong&gt;6.9.0.31 in 2007&lt;&#x2F;strong&gt;; newer versions are donation-gated. The free, actively maintained torch passed to &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;marchaesen&#x2F;vcxsrv&quot;&gt;VcXsrv&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;, an open-source X server built from current X.Org sources. (Note: VcXsrv dropped Windows XP support long ago — that era is genuinely over.)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;4. The whole &lt;em&gt;reason&lt;&#x2F;em&gt; mostly evaporated.&lt;&#x2F;strong&gt; In 2007 you forwarded an editor because editing remote files any other way was painful. In 2026 you’d reach for &lt;strong&gt;VS Code Remote-SSH&lt;&#x2F;strong&gt;, &lt;a href=&quot;&#x2F;post&#x2F;mount-remote-filesystem-sshfs&#x2F;&quot;&gt;mount the remote filesystem with sshfs&lt;&#x2F;a&gt;, or just work in a terminal over &lt;a href=&quot;&#x2F;post&#x2F;ssh-key-login-without-password&#x2F;&quot;&gt;key-based SSH&lt;&#x2F;a&gt;. Forwarding a single GUI window is now the exception, not the workflow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-to-use-today&quot;&gt;What to use today&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;You’re on WSL2 (Windows 11 &#x2F; Win10 21H2+):&lt;&#x2F;strong&gt; do nothing. WSLg is already there. &lt;code&gt;apt install&lt;&#x2F;code&gt; the app and run it. Check with:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;echo $WAYLAND_DISPLAY     # wayland-0   → WSLg is active
echo $DISPLAY             # :0          → XWayland for legacy X11 apps
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;You need to forward a GUI app from a remote Linux server to Windows:&lt;&#x2F;strong&gt; install &lt;strong&gt;VcXsrv&lt;&#x2F;strong&gt;, start it (multiple-windows mode, disable access control on a trusted network only), and:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;ssh -X user@server        # -Y for trusted forwarding if -X is blocked
xterm                     # or whatever GUI app — it paints to VcXsrv
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the &lt;em&gt;exact&lt;&#x2F;em&gt; same mechanism as the Xming clip, just with a modern X server. It still works. It’s still chatty over latency.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;You want remote GUI that doesn’t crawl over WAN:&lt;&#x2F;strong&gt; skip raw X11. Use &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Xpra-org&#x2F;xpra&quot;&gt;&lt;code&gt;xpra&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; (“screen for X” — apps survive disconnects and it compresses well), &lt;code&gt;waypipe&lt;&#x2F;code&gt; for Wayland, or a real remote-desktop protocol (RDP via &lt;code&gt;xrdp&lt;&#x2F;code&gt;, or NoMachine&#x2F;NX) for a full desktop.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;The Windows XP clip is a little time capsule: a tray-icon X server, a PuTTY tunnel, and a Linux text editor materialising on a beige XP desktop. The &lt;em&gt;idea&lt;&#x2F;em&gt; — run the app over there, see it over here — never went away. What changed is that you no longer assemble it by hand. The X server moved inside Windows, X11 gave way to Wayland, and the common case (“I just want to touch files on that box”) got better answers entirely. Eighteen years of plumbing, quietly absorbed into &lt;code&gt;apt install&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;does-ssh-x11-forwarding-still-work-in-2026&quot;&gt;Does SSH X11 forwarding still work in 2026?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — &lt;code&gt;ssh -X&lt;&#x2F;code&gt; (or &lt;code&gt;-Y&lt;&#x2F;code&gt; for trusted forwarding) is unchanged. It forwards X11 to any X server: VcXsrv, Xming, XQuartz, or WSLg’s. It’s just chatty over latency, which is why xpra&#x2F;waypipe&#x2F;RDP usually win for remote work today.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-xming-still-maintained&quot;&gt;Is Xming still maintained?&lt;&#x2F;h3&gt;
&lt;p&gt;Builds still appear (7.7.x in 2026), but they’re donation-gated; the last free public release was 6.9.0.31 in 2007 — the Windows XP era of the clip. For a free, actively built X server on Windows, use VcXsrv.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;do-i-still-need-an-x-server-at-all&quot;&gt;Do I still need an X server at all?&lt;&#x2F;h3&gt;
&lt;p&gt;Only to forward GUI apps from a remote box over SSH, or to run X apps from a non-WSLg distro. On WSL2 with Windows 11 (or Win10 21H2+), WSLg already bundles one — local Linux GUI apps open with nothing extra installed.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-did-wayland-break-the-old-forward-it-over-ssh-trick&quot;&gt;Why did Wayland break the old “forward it over SSH” trick?&lt;&#x2F;h3&gt;
&lt;p&gt;X11 was network-transparent by design, so forwarding came free. Wayland deliberately isn’t — remote display is a separate concern handled by waypipe or RDP. WSLg papers over it by running Weston + XWayland, so both kinds of app work.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;The old way: install &lt;strong&gt;Xming&lt;&#x2F;strong&gt;, tick X11 forwarding in PuTTY, &lt;code&gt;ssh -X&lt;&#x2F;code&gt;, and a remote Linux app paints onto Windows. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=P3hHsfdFusw&quot;&gt;video&lt;&#x2F;a&gt; is that, on Windows XP.&lt;&#x2F;li&gt;
&lt;li&gt;Today: &lt;strong&gt;WSLg&lt;&#x2F;strong&gt; ships a Wayland + X server inside Windows — &lt;code&gt;apt install&lt;&#x2F;code&gt; a Linux app in WSL2 and it just opens.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VcXsrv&lt;&#x2F;strong&gt; is the free, maintained successor to Xming for the classic remote-forwarding case; &lt;strong&gt;Xming&lt;&#x2F;strong&gt; itself went donation-only after 6.9.0.31 (2007).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Wayland&lt;&#x2F;strong&gt; replaced X11’s network transparency; use &lt;strong&gt;waypipe&lt;&#x2F;strong&gt;, &lt;strong&gt;xpra&lt;&#x2F;strong&gt;, or RDP for fast remote GUI.&lt;&#x2F;li&gt;
&lt;li&gt;For the original motivation — editing on a remote box — prefer VS Code Remote-SSH, &lt;a href=&quot;&#x2F;post&#x2F;mount-remote-filesystem-sshfs&#x2F;&quot;&gt;sshfs&lt;&#x2F;a&gt;, or plain &lt;a href=&quot;&#x2F;post&#x2F;ssh-key-login-without-password&#x2F;&quot;&gt;SSH&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Extending video clips with AI on a 12 GB GPU — six models compared</title>
        <published>2026-06-07T00:00:00+00:00</published>
        <updated>2026-06-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/video-extension-models-12gb-gpu/"/>
        <id>https://www.x2q.net/post/video-extension-models-12gb-gpu/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/video-extension-models-12gb-gpu/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; “Extending” a video clip is really &lt;strong&gt;image-to-video&lt;&#x2F;strong&gt;: grab the last frame and let a model generate a believable continuation, then concatenate. I ran the same machine shot through &lt;strong&gt;six&lt;&#x2F;strong&gt; I2V models on one &lt;strong&gt;12 GB RTX 4070 Ti&lt;&#x2F;strong&gt;: Wan2.2-TI2V-5B, Wan2.2-I2V-A14B (GGUF Q4, at 640 px and at 720p), Stable Video Diffusion, LTX-Video and CogVideoX-5B. The headline isn’t the winner — it’s that a &lt;strong&gt;14B&lt;&#x2F;strong&gt; model and &lt;strong&gt;720p&lt;&#x2F;strong&gt; both fit in 12 GB once you get the &lt;strong&gt;offload strategy&lt;&#x2F;strong&gt; right.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-task&quot;&gt;The task&lt;&#x2F;h2&gt;
&lt;p&gt;I had a stack of short clips of forestry machines — an excavator digging, a wood chipper, a wheel loader — and wanted them a couple of seconds longer. There’s no more footage; the only way to extend is to &lt;em&gt;invent&lt;&#x2F;em&gt; it. Modern image-to-video models do exactly this: condition on a starting image (here, the clip’s final frame) and a text prompt, and hallucinate motion forward. Stitch the real tail to the generated continuation and you have a longer clip.&lt;&#x2F;p&gt;
&lt;p&gt;Everything below ran locally on a single 12 GB RTX 4070 Ti with 125 GB of system RAM (the RAM matters — it’s where the offloaded weights live).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-real-problem-fitting-big-video-models-in-12-gb&quot;&gt;The real problem: fitting big video models in 12 GB&lt;&#x2F;h2&gt;
&lt;p&gt;Image diffusion is forgiving on VRAM. Video diffusion is not: you’re denoising a 3D latent (frames × height × width), and the newest quality models are 5B-14B parameters. Three things kept biting:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The text encoder is huge.&lt;&#x2F;strong&gt; Wan and LTX and CogVideoX all use a T5-XXL &#x2F; umT5-XXL encoder that’s ~11 GB on its own. The instant model-level offload moves it onto a 12 GB card to encode the prompt, you OOM — before the video model even runs.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Activations, not weights, blow up at higher resolution.&lt;&#x2F;strong&gt; A 9 GB transformer can sit on the card fine; it’s the attention activations over (frames × tokens) that overflow when you push past ~480p.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Quantization helps disk and a bit of VRAM, but interacts badly with some offload modes.&lt;&#x2F;strong&gt; GGUF Q4 shrinks a 14B expert to ~9 GB, but the way you offload it matters (see below).&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The fixes that made it all work:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pre-encode the prompt on CPU, then evict the encoder.&lt;&#x2F;strong&gt; Compute the text embeddings with the T5 on the CPU &lt;em&gt;in fp32&lt;&#x2F;em&gt; (bf16 matmuls on CPU are pathologically slow — it looks hung), cast the result to bf16, set &lt;code&gt;text_encoder = None&lt;&#x2F;code&gt;, and only then start the video model. Now the 11 GB encoder never touches the GPU.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pick the offload mode per model:&lt;&#x2F;strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enable_model_cpu_offload()&lt;&#x2F;code&gt; — moves whole sub-models on&#x2F;off the GPU. Fast, but one model must fit at a time (so it caps resolution&#x2F;length).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;enable_sequential_cpu_offload()&lt;&#x2F;code&gt; — streams sub-modules. Lower peak VRAM, slower. Great for non-quantized models (LTX, CogVideoX), but &lt;strong&gt;breaks GGUF&lt;&#x2F;strong&gt; (accelerate’s meta-device step drops the quant type).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;apply_group_offloading(..., offload_type=&quot;leaf_level&quot;, use_stream=True)&lt;&#x2F;code&gt; — streams the transformer leaf-by-leaf. This is the secret weapon.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;wan2-2-from-fake-5b-to-real-720p&quot;&gt;Wan2.2: from “fake” 5B to real 720p&lt;&#x2F;h2&gt;
&lt;p&gt;I started with &lt;strong&gt;Wan2.2-TI2V-5B&lt;&#x2F;strong&gt; (the small, consumer-friendly one). It runs easily with model-offload and produces coherent motion, but at the ~640 px the card allows it looks soft — the classic “AI video” plasticky feel.&lt;&#x2F;p&gt;
&lt;p&gt;Stepping up to &lt;strong&gt;Wan2.2-I2V-A14B&lt;&#x2F;strong&gt; meant quantization. The official 14B is two experts (high-noise + low-noise) and wants 24-35 GB in fp16. The GGUF Q4 build is ~9 GB per expert, and diffusers can load each through &lt;code&gt;WanTransformer3DModel.from_single_file(..., quantization_config=GGUFQuantizationConfig(...))&lt;&#x2F;code&gt; — you just need the &lt;code&gt;gguf&lt;&#x2F;code&gt; package, or it fails to read the file. With model-offload + the CPU-prompt trick, the 14B runs at 640 px and the motion is visibly better (the wood chipper, a chaotic close-up that the 5B turned to mush, came out coherent).&lt;&#x2F;p&gt;
&lt;p&gt;Then the actual goal: &lt;strong&gt;720p&lt;&#x2F;strong&gt;. Model-offload at 1280×720 peaks at ~11.7 GB — it fits, but only ~13 frames before the activations overflow. The breakthrough was &lt;strong&gt;group-leaf offloading&lt;&#x2F;strong&gt;: stream the transformer one leaf module at a time, so almost nothing sits on the GPU. Peak VRAM at 1280×720: &lt;strong&gt;1.9 GB&lt;&#x2F;strong&gt;. The card is suddenly empty and resolution is free; the cost is speed (leaf streaming is slow, ~20 min a clip). But it’s real, sharp 720p on a 12 GB card.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;from diffusers.hooks import apply_group_offloading
for t in (pipe.transformer, pipe.transformer_2):  # both noise experts
    apply_group_offloading(t, onload_device=torch.device(&amp;quot;cuda&amp;quot;),
                           offload_device=torch.device(&amp;quot;cpu&amp;quot;),
                           offload_type=&amp;quot;leaf_level&amp;quot;, use_stream=True)
pipe.vae.enable_tiling()
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;the-other-three&quot;&gt;The other three&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LTX-Video&lt;&#x2F;strong&gt; (2B). Fast and, for short extensions, the best of the lot — dynamic motion (the excavator throws dirt), prompt-guided, ~3 min a clip at 768×512. Needs &lt;code&gt;sentencepiece&lt;&#x2F;code&gt; for its T5 tokenizer, and &lt;code&gt;sequential&lt;&#x2F;code&gt; offload because that T5 is the same 11 GB OOM trap.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stable Video Diffusion&lt;&#x2F;strong&gt; (img2vid-xt). The veteran. Runs at 1024×576 with model-offload + &lt;code&gt;unet.enable_forward_chunking()&lt;&#x2F;code&gt;, and it’s quick — but SVD has &lt;strong&gt;no text prompt&lt;&#x2F;strong&gt;, so its motion is &lt;em&gt;ambient&lt;&#x2F;em&gt; (a gentle drift, a breeze in the trees) rather than the directed action you asked for. Great for cinematic cutaways, wrong tool for “keep digging.”&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CogVideoX-5B-I2V&lt;&#x2F;strong&gt;. The quality surprise: the longest and smoothest clip (6 s, 49 frames at 720×480), coherent throughout. The catch is speed — with sequential offload + VAE tiling it’s roughly &lt;strong&gt;an hour per clip&lt;&#x2F;strong&gt; on 12 GB, which makes it impractical here even though the output is lovely.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-scoreboard&quot;&gt;The scoreboard&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model&lt;&#x2F;th&gt;&lt;th&gt;Params&lt;&#x2F;th&gt;&lt;th&gt;Fits 12 GB via&lt;&#x2F;th&gt;&lt;th&gt;Output&lt;&#x2F;th&gt;&lt;th&gt;Speed&lt;&#x2F;th&gt;&lt;th&gt;Verdict&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;LTX-Video&lt;&#x2F;td&gt;&lt;td&gt;2B&lt;&#x2F;td&gt;&lt;td&gt;sequential offload&lt;&#x2F;td&gt;&lt;td&gt;768×512&lt;&#x2F;td&gt;&lt;td&gt;~3 min&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Best for short extensions&lt;&#x2F;strong&gt; — fast, dynamic&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wan2.2-I2V-14B @ 720p&lt;&#x2F;td&gt;&lt;td&gt;14B (Q4)&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;group-leaf offload (1.9 GB!)&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;1280×720&lt;&#x2F;td&gt;&lt;td&gt;~20 min&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Real 720p on 12 GB&lt;&#x2F;strong&gt; — sharpest&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wan2.2-I2V-14B&lt;&#x2F;td&gt;&lt;td&gt;14B (Q4)&lt;&#x2F;td&gt;&lt;td&gt;model offload + CPU prompt&lt;&#x2F;td&gt;&lt;td&gt;640×384&lt;&#x2F;td&gt;&lt;td&gt;~8 min&lt;&#x2F;td&gt;&lt;td&gt;Good motion, esp. the hard cases&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CogVideoX-5B-I2V&lt;&#x2F;td&gt;&lt;td&gt;5B&lt;&#x2F;td&gt;&lt;td&gt;sequential + VAE tiling&lt;&#x2F;td&gt;&lt;td&gt;720×480&lt;&#x2F;td&gt;&lt;td&gt;~1 hr&lt;&#x2F;td&gt;&lt;td&gt;Longest&#x2F;smoothest, but impractically slow here&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Stable Video Diffusion&lt;&#x2F;td&gt;&lt;td&gt;~1.5B&lt;&#x2F;td&gt;&lt;td&gt;model offload + chunking&lt;&#x2F;td&gt;&lt;td&gt;1024×576&lt;&#x2F;td&gt;&lt;td&gt;~2 min&lt;&#x2F;td&gt;&lt;td&gt;Fast &amp;amp; clean, but motion is ambient&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Wan2.2-TI2V-5B&lt;&#x2F;td&gt;&lt;td&gt;5B&lt;&#x2F;td&gt;&lt;td&gt;model offload&lt;&#x2F;td&gt;&lt;td&gt;640×384&lt;&#x2F;td&gt;&lt;td&gt;~3 min&lt;&#x2F;td&gt;&lt;td&gt;The soft, “fake”-looking baseline&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;what-i-d-actually-do&quot;&gt;What I’d actually do&lt;&#x2F;h2&gt;
&lt;p&gt;On a 12 GB card: reach for &lt;strong&gt;LTX-Video&lt;&#x2F;strong&gt; for quick, directed extensions, and &lt;strong&gt;Wan2.2-14B + group-offload&lt;&#x2F;strong&gt; when you specifically need &lt;strong&gt;720p&lt;&#x2F;strong&gt; and can wait. SVD stays in the kit for ambient cutaways. CogVideoX is gorgeous but you’d want a bigger GPU to use it for real.&lt;&#x2F;p&gt;
&lt;p&gt;And the honest limitation: the thing that still reads as “AI” is mostly &lt;strong&gt;resolution&lt;&#x2F;strong&gt;. Group-offload buys you 720p &lt;em&gt;or&lt;&#x2F;em&gt; a 12 GB card buys you length — not both at once. High-res &lt;em&gt;and&lt;&#x2F;em&gt; multi-second is the one wall a 12 GB GPU won’t climb; that’s what a 24 GB+ card (or an hour of cloud) is for. Everything else — even a 14B model at 720p — turned out to be a question of offloading, not horsepower.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>8-digit BINs explained: what the IIN expansion means for card BIN lookups (2026)</title>
        <published>2026-06-06T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/8-digit-bin-iin-explained/"/>
        <id>https://www.x2q.net/post/8-digit-bin-iin-explained/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/8-digit-bin-iin-explained/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; the &lt;strong&gt;IIN&lt;&#x2F;strong&gt; (issuer identification number) on a payment card grew from &lt;strong&gt;6 digits to 8&lt;&#x2F;strong&gt;. A 6-digit prefix can now map to &lt;em&gt;several&lt;&#x2F;em&gt; issuers or products, so BIN lookups that key on 6 digits return ambiguous results. The card number’s overall length didn’t change — the account portion just got two digits shorter.&lt;&#x2F;p&gt;
&lt;p&gt;This is the structural background behind &lt;a href=&quot;&#x2F;post&#x2F;binlist-net-iinlist-com-story&#x2F;&quot;&gt;binlist.net and iinlist.com&lt;&#x2F;a&gt; — if you do anything with card numbers, it’s worth understanding.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-a-bin-iin-actually-is&quot;&gt;What a BIN&#x2F;IIN actually is&lt;&#x2F;h2&gt;
&lt;p&gt;The first digits of a card’s PAN (primary account number) identify who issued it. Two terms for the same thing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BIN&lt;&#x2F;strong&gt; — &lt;em&gt;Bank Identification Number&lt;&#x2F;em&gt;, the older industry term.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IIN&lt;&#x2F;strong&gt; — &lt;em&gt;Issuer Identification Number&lt;&#x2F;em&gt;, the term ISO&#x2F;IEC 7812 uses.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;They’re used interchangeably. The very first digit is the &lt;strong&gt;MII&lt;&#x2F;strong&gt; (Major Industry Identifier) — &lt;code&gt;4&lt;&#x2F;code&gt; = Visa, &lt;code&gt;5&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;2&lt;&#x2F;code&gt; = Mastercard, &lt;code&gt;3&lt;&#x2F;code&gt; = Amex&#x2F;Diners&#x2F;JCB, and so on. The PAN as a whole is structured as:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;   IIN (issuer)        account identifier        check digit (Luhn)
┌────────────────┐ ┌───────────────────────┐ ┌──┐
4 5 3 9 1 4 8 8   0 3 4 5 6 7 8             9
└─ 8 digits now ─┘
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;what-changed&quot;&gt;What changed&lt;&#x2F;h2&gt;
&lt;p&gt;Historically the IIN was the &lt;strong&gt;first 6 digits&lt;&#x2F;strong&gt;. ISO&#x2F;IEC 7812 was updated to define it as &lt;strong&gt;8 digits&lt;&#x2F;strong&gt;, and the card networks moved the ecosystem onto 8-digit BINs — &lt;strong&gt;Visa and Mastercard set April 2022&lt;&#x2F;strong&gt; as the point by which issuers, acquirers, and processors had to support the longer identifier.&lt;&#x2F;p&gt;
&lt;p&gt;Crucially, the &lt;strong&gt;PAN length didn’t change&lt;&#x2F;strong&gt; (still up to 19 digits, 16 for most cards). The issuer identifier took two more digits, so the &lt;strong&gt;account-identifier portion got two digits shorter&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-they-did-it&quot;&gt;Why they did it&lt;&#x2F;h2&gt;
&lt;p&gt;They were running out of room. A 6-digit space is one million possible IINs, and between new fintechs, BIN sponsorship, and ever-finer product segmentation (each card product often wants its own range) the 6-digit pool was being exhausted. Going to 8 digits multiplies the available ranges by a hundred.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-practical-fallout-for-lookups&quot;&gt;The practical fallout for lookups&lt;&#x2F;h2&gt;
&lt;p&gt;This is the part that bites if you maintain or consume a BIN database:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A 6-digit prefix is no longer unique.&lt;&#x2F;strong&gt; Within one old 6-digit block, the two extra digits can now belong to &lt;em&gt;different&lt;&#x2F;em&gt; issuers or &lt;em&gt;different&lt;&#x2F;em&gt; products. Looking up only 6 digits can return the wrong bank, country, or card type.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;You need at least 8 digits to be confident.&lt;&#x2F;strong&gt; If your input only has 6 (common with masked&#x2F;truncated data), treat the result as a best-effort guess, not ground truth.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Your data source has to be 8-digit aware.&lt;&#x2F;strong&gt; A BIN list that only carries 6-digit granularity silently collapses distinct 8-digit issuers into one row.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre&gt;&lt;code&gt;# Same 6-digit prefix, two different 8-digit issuers (illustrative):
4539 14 ..  -&amp;gt; 45391488 = Issuer A, debit,  country X
4539 14 ..  -&amp;gt; 45391499 = Issuer B, credit, country Y
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;what-to-do-in-practice&quot;&gt;What to do in practice&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Capture and key on 8 digits&lt;&#x2F;strong&gt; wherever you legitimately have them.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Degrade gracefully&lt;&#x2F;strong&gt; when you only have 6 — return the prefix-level info and flag it as low-confidence rather than asserting a single issuer.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mind PCI scope.&lt;&#x2F;strong&gt; The first 6 &lt;em&gt;and&lt;&#x2F;em&gt; the first 8 are both allowed to be stored&#x2F;displayed under PCI DSS truncation rules (BIN + last 4), but check your acquirer’s current guidance before widening what you persist.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Don’t hardcode “6”.&lt;&#x2F;strong&gt; If you have regexes or schema columns that assume a 6-digit BIN, they’re now wrong.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;did-my-card-number-get-longer&quot;&gt;Did my card number get longer?&lt;&#x2F;h3&gt;
&lt;p&gt;No. The total PAN length is unchanged. Only the &lt;em&gt;split&lt;&#x2F;em&gt; between issuer-identifier and account-identifier moved — issuer up by two digits, account down by two.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-it-bin-or-iin&quot;&gt;Is it “BIN” or “IIN”?&lt;&#x2F;h3&gt;
&lt;p&gt;Same thing. “BIN” is the everyday industry word; “IIN” is the formal ISO term. Expect to see both, often in the same document.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-still-do-a-6-digit-lookup&quot;&gt;Can I still do a 6-digit lookup?&lt;&#x2F;h3&gt;
&lt;p&gt;You can, and for many ranges it’s still fine — but it’s no longer &lt;em&gt;guaranteed&lt;&#x2F;em&gt; to identify a single issuer. Treat 6-digit results as approximate and prefer 8 when you have them.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-do-i-get-8-digit-bin-data&quot;&gt;Where do I get 8-digit BIN data?&lt;&#x2F;h3&gt;
&lt;p&gt;A maintained BIN&#x2F;IIN database that carries 8-digit granularity. The free &lt;a href=&quot;&#x2F;post&#x2F;binlist-net-iinlist-com-story&#x2F;&quot;&gt;binlist.net&lt;&#x2F;a&gt; and the commercial iinlist.com exist precisely for this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;The issuer identifier on a card went from &lt;strong&gt;6 to 8 digits&lt;&#x2F;strong&gt; (networks: support required by ~April 2022).&lt;&#x2F;li&gt;
&lt;li&gt;Total card-number length is unchanged; the account portion shrank by two digits.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;6-digit BIN lookups are now ambiguous&lt;&#x2F;strong&gt; — capture and match on 8 digits where you can, and flag 6-digit results as low-confidence.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Find which process is using a port on Linux (ss, lsof, fuser) — 2026</title>
        <published>2026-06-05T00:00:00+00:00</published>
        <updated>2026-06-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/find-process-using-port-linux/"/>
        <id>https://www.x2q.net/post/find-process-using-port-linux/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/find-process-using-port-linux/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;sudo ss -ltnp | grep :8080&lt;&#x2F;code&gt; shows the process listening on port 8080. Then &lt;code&gt;kill &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt;. &lt;code&gt;ss&lt;&#x2F;code&gt; is the modern replacement for the deprecated &lt;code&gt;netstat&lt;&#x2F;code&gt;; &lt;code&gt;lsof -i :8080&lt;&#x2F;code&gt; and &lt;code&gt;fuser 8080&#x2F;tcp&lt;&#x2F;code&gt; do the same job.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ss-the-modern-way&quot;&gt;ss — the modern way&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;ss&lt;&#x2F;code&gt; (from &lt;code&gt;iproute2&lt;&#x2F;code&gt;) ships on every current distro and replaced &lt;code&gt;netstat&lt;&#x2F;code&gt;. To see what’s &lt;strong&gt;listening&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo ss -ltnp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-l&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; listening sockets&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-t&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; TCP (use &lt;code&gt;-u&lt;&#x2F;code&gt; for UDP)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-n&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; numeric — don’t resolve ports to names (faster, clearer)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-p&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; show the &lt;strong&gt;p&lt;&#x2F;strong&gt;rocess (needs root to see other users’ processes)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Filter to one port with &lt;code&gt;ss&lt;&#x2F;code&gt;’s own expression (no grep needed):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo ss -ltnp &amp;#39;sport = :8080&amp;#39;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       511           0.0.0.0:8080       0.0.0.0:*      users:((&amp;quot;node&amp;quot;,pid=4123,fd=18))
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There’s your PID: &lt;code&gt;4123&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lsof-the-alternative&quot;&gt;lsof — the alternative&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo lsof -i :8080            # anything using port 8080
sudo lsof -iTCP:8080 -sTCP:LISTEN   # only the listener
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;lsof&lt;&#x2F;code&gt; prints the command, PID, and user per line. It isn’t installed everywhere by default (&lt;code&gt;sudo apt install lsof&lt;&#x2F;code&gt;), but it’s the most readable for “who has this port &lt;em&gt;and&lt;&#x2F;em&gt; in what state”.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fuser-the-quick-one&quot;&gt;fuser — the quick one&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo fuser 8080&#x2F;tcp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Prints just the PID(s). It can even kill in one shot:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo fuser -k 8080&#x2F;tcp       # send SIGKILL to whatever holds 8080&#x2F;tcp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Convenient, but blunt — it’ll &lt;code&gt;kill -9&lt;&#x2F;code&gt; without asking, so know what you’re terminating first.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;netstat-only-if-you-re-stuck-on-an-old-box&quot;&gt;netstat — only if you’re stuck on an old box&lt;&#x2F;h2&gt;
&lt;p&gt;The classic &lt;code&gt;netstat -tulpn&lt;&#x2F;code&gt; still works where it’s installed, but &lt;code&gt;netstat&lt;&#x2F;code&gt; is deprecated and missing from minimal modern images. Prefer &lt;code&gt;ss&lt;&#x2F;code&gt;; the flags are nearly identical.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;free-the-port&quot;&gt;Free the port&lt;&#x2F;h2&gt;
&lt;p&gt;Once you have the PID, stop it gracefully first, then escalate only if it ignores you:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;kill 4123          # SIGTERM — lets it shut down cleanly
kill -9 4123       # SIGKILL — last resort, no cleanup
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If it’s a managed service, stop it properly so it doesn’t just respawn:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop myapp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No process shown?&lt;&#x2F;strong&gt; You’re not root. The &lt;code&gt;-p&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-i&lt;&#x2F;code&gt; process columns are empty for sockets you don’t own — re-run with &lt;code&gt;sudo&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Port still “in use” after the process died.&lt;&#x2F;strong&gt; A closed TCP socket lingers in &lt;code&gt;TIME_WAIT&lt;&#x2F;code&gt; for a couple of minutes. Either wait, or set &lt;code&gt;SO_REUSEADDR&lt;&#x2F;code&gt; in your server (most frameworks have a “reuse address” flag).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;It came back immediately.&lt;&#x2F;strong&gt; A supervisor (systemd, Docker, pm2) restarted it. Stop the supervisor unit, not the child PID.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 vs IPv4.&lt;&#x2F;strong&gt; &lt;code&gt;0.0.0.0:8080&lt;&#x2F;code&gt; is IPv4; &lt;code&gt;[::]:8080&lt;&#x2F;code&gt; is IPv6. A process can hold both — &lt;code&gt;ss -ltnp&lt;&#x2F;code&gt; lists each separately.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-do-i-see-established-connections-not-just-listeners&quot;&gt;How do I see established connections, not just listeners?&lt;&#x2F;h3&gt;
&lt;p&gt;Drop &lt;code&gt;-l&lt;&#x2F;code&gt;: &lt;code&gt;sudo ss -tnp&lt;&#x2F;code&gt; shows active connections. Add &lt;code&gt;state established&lt;&#x2F;code&gt; to filter.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-s-using-a-udp-port&quot;&gt;What’s using a UDP port?&lt;&#x2F;h3&gt;
&lt;p&gt;Swap &lt;code&gt;-t&lt;&#x2F;code&gt; for &lt;code&gt;-u&lt;&#x2F;code&gt;: &lt;code&gt;sudo ss -lunp &#x27;sport = :53&#x27;&lt;&#x2F;code&gt; (e.g. to find what’s on DNS).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;one-liner-to-kill-whatever-is-on-a-port&quot;&gt;One-liner to kill whatever is on a port?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;sudo fuser -k 8080&#x2F;tcp&lt;&#x2F;code&gt;, or &lt;code&gt;sudo kill $(sudo lsof -t -i:8080)&lt;&#x2F;code&gt;. Use with care.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Find it: &lt;code&gt;sudo ss -ltnp &#x27;sport = :8080&#x27;&lt;&#x2F;code&gt; (modern), &lt;code&gt;sudo lsof -i :8080&lt;&#x2F;code&gt;, or &lt;code&gt;sudo fuser 8080&#x2F;tcp&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Free it: &lt;code&gt;kill &amp;lt;pid&amp;gt;&lt;&#x2F;code&gt; first, &lt;code&gt;kill -9&lt;&#x2F;code&gt; only if needed, or stop the systemd unit.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ss&lt;&#x2F;code&gt; replaces &lt;code&gt;netstat&lt;&#x2F;code&gt; — same idea, fewer surprises on modern systems.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Local speech-to-text with whisper.cpp — a self-hosted Google Speech API alternative (2026)</title>
        <published>2026-06-04T00:00:00+00:00</published>
        <updated>2026-06-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/local-speech-to-text-whisper-cpp/"/>
        <id>https://www.x2q.net/post/local-speech-to-text-whisper-cpp/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/local-speech-to-text-whisper-cpp/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;whisper.cpp&lt;&#x2F;code&gt; runs OpenAI’s Whisper speech-to-text models locally. Build it, download a model, convert your audio to 16 kHz mono WAV with &lt;code&gt;ffmpeg&lt;&#x2F;code&gt;, and run &lt;code&gt;whisper-cli -m models&#x2F;ggml-base.en.bin -f audio.wav&lt;&#x2F;code&gt;. No API key, no upload, no per-minute charge.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This blog has a history of &lt;a href=&quot;&#x2F;&quot;&gt;speech-to-text experiments&lt;&#x2F;a&gt; — the old ones leaned on the Google Speech API. The local-model story is dramatically better now: same quality tier, zero cloud dependency.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;why-local&quot;&gt;Why local&lt;&#x2F;h2&gt;
&lt;p&gt;A hosted speech API means keys, per-minute billing, and shipping (often sensitive) audio to a third party. &lt;code&gt;whisper.cpp&lt;&#x2F;code&gt; is a small, dependency-light C&#x2F;C++ port of Whisper that runs the same models offline — on CPU, or accelerated with Metal (Mac) &#x2F; CUDA (NVIDIA). For batch transcription, private recordings, or just avoiding a metered API, it’s the obvious default in 2026.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;build-it&quot;&gt;Build it&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&#x2F;&#x2F;github.com&#x2F;ggml-org&#x2F;whisper.cpp
cd whisper.cpp
cmake -B build
cmake --build build -j --config Release
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The CLI lands at &lt;code&gt;build&#x2F;bin&#x2F;whisper-cli&lt;&#x2F;code&gt;. (Older checkouts called this binary &lt;code&gt;.&#x2F;main&lt;&#x2F;code&gt; — if a guide references &lt;code&gt;main&lt;&#x2F;code&gt;, it’s the same tool under the previous name.)&lt;&#x2F;p&gt;
&lt;p&gt;On a Mac, Metal acceleration is on by default. For an NVIDIA GPU, configure with CUDA:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cmake -B build -DGGML_CUDA=1
cmake --build build -j --config Release
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;get-a-model&quot;&gt;Get a model&lt;&#x2F;h2&gt;
&lt;p&gt;Models are GGML&#x2F;GGUF files you download once. The helper script fetches them into &lt;code&gt;models&#x2F;&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sh .&#x2F;models&#x2F;download-ggml-model.sh base.en
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Pick the size for your accuracy&#x2F;speed trade-off:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tiny.en&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;base.en&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — fast, fine for clean English; great on a laptop CPU.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;small.en&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — a noticeable accuracy step up, still quick.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;medium&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — strong, multilingual.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;large-v3&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — best quality, wants a GPU and a few GB of RAM.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Drop the &lt;code&gt;.en&lt;&#x2F;code&gt; suffix for the multilingual variants. Quantized builds (e.g. &lt;code&gt;base.en-q5_0&lt;&#x2F;code&gt;) cut memory with minimal quality loss.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;convert-your-audio&quot;&gt;Convert your audio&lt;&#x2F;h2&gt;
&lt;p&gt;Whisper expects &lt;strong&gt;16 kHz mono PCM WAV&lt;&#x2F;strong&gt;. Convert anything to that with &lt;code&gt;ffmpeg&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ffmpeg -i recording.mp3 -ar 16000 -ac 1 -c:a pcm_s16le recording.wav
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-ar 16000&lt;&#x2F;code&gt; — 16 kHz sample rate&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-ac 1&lt;&#x2F;code&gt; — mono&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-c:a pcm_s16le&lt;&#x2F;code&gt; — 16-bit PCM&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;transcribe&quot;&gt;Transcribe&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;.&#x2F;build&#x2F;bin&#x2F;whisper-cli -m models&#x2F;ggml-base.en.bin -f recording.wav
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It prints the transcript with timestamps. Write it to files instead:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.&#x2F;build&#x2F;bin&#x2F;whisper-cli -m models&#x2F;ggml-base.en.bin -f recording.wav \
  -otxt -osrt -ovtt -oj
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-otxt&lt;&#x2F;code&gt; plain text · &lt;code&gt;-osrt&lt;&#x2F;code&gt; SRT subtitles · &lt;code&gt;-ovtt&lt;&#x2F;code&gt; WebVTT · &lt;code&gt;-oj&lt;&#x2F;code&gt; JSON (with word timing).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For non-English audio, use a multilingual model and set the language (or let it auto-detect):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;.&#x2F;build&#x2F;bin&#x2F;whisper-cli -m models&#x2F;ggml-medium.bin -l da -f optagelse.wav
# -l auto  to detect automatically; add --translate to render English
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;speed-notes&quot;&gt;Speed notes&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tiny&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;base&lt;&#x2F;code&gt; transcribe faster than real time on a modern laptop CPU. &lt;code&gt;large-v3&lt;&#x2F;code&gt; wants a GPU to be comfortable.&lt;&#x2F;li&gt;
&lt;li&gt;Long file? Whisper chunks internally, but you can pre-split with &lt;code&gt;ffmpeg -f segment&lt;&#x2F;code&gt; for parallelism across cores.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--threads N&lt;&#x2F;code&gt; tunes CPU usage; Metal&#x2F;CUDA builds offload the heavy matmuls automatically.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-this-the-same-model-as-openai-s-whisper&quot;&gt;Is this the same model as OpenAI’s Whisper?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — &lt;code&gt;whisper.cpp&lt;&#x2F;code&gt; runs the released Whisper weights (converted to GGML&#x2F;GGUF). The output quality matches the corresponding model size; only the runtime differs.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;do-i-need-a-gpu&quot;&gt;Do I need a GPU?&lt;&#x2F;h3&gt;
&lt;p&gt;No. &lt;code&gt;tiny&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;base&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;small&lt;&#x2F;code&gt; are perfectly usable on CPU. A GPU mainly helps with &lt;code&gt;medium&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;large&lt;&#x2F;code&gt; and long batches.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-accurate-is-it-versus-a-cloud-speech-api&quot;&gt;How accurate is it versus a cloud speech API?&lt;&#x2F;h3&gt;
&lt;p&gt;For clean audio, &lt;code&gt;small.en&lt;&#x2F;code&gt; and up is competitive with the major hosted APIs, and &lt;code&gt;large-v3&lt;&#x2F;code&gt; generally beats them — with the bonus that nothing leaves your machine.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-it-do-live-streaming-transcription&quot;&gt;Can it do live&#x2F;streaming transcription?&lt;&#x2F;h3&gt;
&lt;p&gt;There’s a &lt;code&gt;stream&lt;&#x2F;code&gt; example in the repo that does near-real-time from a microphone. It’s rougher than batch mode but works for live captions.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Build &lt;code&gt;whisper.cpp&lt;&#x2F;code&gt;, download a model with &lt;code&gt;download-ggml-model.sh&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Convert audio to 16 kHz mono WAV with &lt;code&gt;ffmpeg&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;whisper-cli -m models&#x2F;ggml-base.en.bin -f audio.wav&lt;&#x2F;code&gt; — local, key-free transcription.&lt;&#x2F;li&gt;
&lt;li&gt;Scale the model size to your hardware; use &lt;code&gt;-l&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;--translate&lt;&#x2F;code&gt; for other languages.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Five ways to get b-roll from one ride — all on a local GPU</title>
        <published>2026-06-02T00:00:00+00:00</published>
        <updated>2026-06-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/ai-broll-five-ways-local-gpu/"/>
        <id>https://www.x2q.net/post/ai-broll-five-ways-local-gpu/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/ai-broll-five-ways-local-gpu/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; B-roll is the cutaway footage that lets an edit breathe, and onboard POV footage from a single camera has exactly none of it. I took one ATV ride and tried &lt;strong&gt;five&lt;&#x2F;strong&gt; ways to manufacture b-roll from it: (1) &lt;strong&gt;auto-mine&lt;&#x2F;strong&gt; the best real frames with OpenCV, (2) &lt;strong&gt;generate stills&lt;&#x2F;strong&gt; and seed them from real frames with &lt;strong&gt;SDXL-Turbo → RealVisXL img2img&lt;&#x2F;strong&gt;, (3) fake a cinematic camera move with &lt;strong&gt;Depth-Anything-V2&lt;&#x2F;strong&gt; 2.5D parallax, (4) generate &lt;em&gt;actual motion&lt;&#x2F;em&gt; with &lt;strong&gt;Stable Video Diffusion&lt;&#x2F;strong&gt;, and (5) make a &lt;strong&gt;slow-motion&lt;&#x2F;strong&gt; cut of the real footage with optical-flow interpolation. Everything ran locally on a 12 GB RTX 4070 Ti. They’re complementary — here’s the comparison and the traps.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h2&gt;
&lt;p&gt;Onboard footage — a GoPro&#x2F;DJI on the handlebars of a go-kart or an ATV — is one continuous first-person shot. It’s great for the action, but an edit needs &lt;em&gt;cutaways&lt;&#x2F;em&gt;: the establishing shot, the scenery, the slow detail that gives the viewer a breath between the intense bits. With one camera and no B-cam, you have none. So: can you manufacture believable b-roll after the fact, locally, for free?&lt;&#x2F;p&gt;
&lt;p&gt;I tried five approaches on a single 11-minute ATV tour through Nordic forest and gravel trails. Each produces a short graded reel; the interesting part is the trade-offs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-auto-mine-the-real-footage-opencv&quot;&gt;1. Auto-mine the real footage (OpenCV)&lt;&#x2F;h2&gt;
&lt;p&gt;The cheapest b-roll is the b-roll you already shot but didn’t notice. A small OpenCV pass samples ~1,400 frames across the ride and scores each for &lt;strong&gt;sharpness&lt;&#x2F;strong&gt; (variance of the Laplacian), &lt;strong&gt;colourfulness&lt;&#x2F;strong&gt; (the Hasler–Süsstrunk metric), &lt;strong&gt;exposure&lt;&#x2F;strong&gt; (distance from a mid-tone target) and &lt;strong&gt;motion-stability&lt;&#x2F;strong&gt; (inter-frame difference — low is good for a stable cutaway). Non-maximum suppression then spreads the picks out so you don’t get eight near-identical frames from the same 20 seconds.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;score = 1.1*z(sharp) + 1.0*z(colourful) + 0.8*z(exposure) - 0.9*shake - 6.0*clipping
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It’s not glamorous, but it’s instant, free, and 100% authentic. The output is genuinely your ride — just the most b-roll-worthy three seconds you forgot you filmed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-generative-stills-matched-to-the-real-ride&quot;&gt;2. Generative stills, matched to the real ride&lt;&#x2F;h2&gt;
&lt;p&gt;This is where it got interesting — and where it first went wrong.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Attempt one: pure text-to-image.&lt;&#x2F;strong&gt; SDXL-Turbo with prompts like &lt;em&gt;“cinematic misty forest at golden hour”&lt;&#x2F;em&gt; produced beautiful images that looked nothing like the actual ride. Wrong light (sunset vs. the real overcast), no quad in frame, no POV. Gorgeous stock footage for &lt;em&gt;a&lt;&#x2F;em&gt; forest, not &lt;em&gt;this&lt;&#x2F;em&gt; one.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Attempt two: img2img, seeded from real frames.&lt;&#x2F;strong&gt; Feeding a real frame in as the init image at moderate denoising strength keeps the real composition — the black quad’s handlebars and mirrors in the foreground, the grass-and-dirt two-track, the overcast palette — and regenerates the detail. Now it intercuts with the real footage. But SDXL-&lt;strong&gt;Turbo&lt;&#x2F;strong&gt; at strength ≈0.5 still &lt;em&gt;melted&lt;&#x2F;em&gt;: it hallucinated extra gauges onto the dashboard and warped the handlebars. The classic “AI look.”&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Attempt three: better models, lower strength.&lt;&#x2F;strong&gt; I compared three less-melty methods on the same frames:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;ai-broll&#x2F;compare-methods.avif&quot; alt=&quot;Real frame (top-left) versus three generative methods: SDXL low-strength img2img, ControlNet-Canny, and RealVisXL. SDXL low-strength and RealVisXL stay faithful; ControlNet recomposes greener.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SDXL-base, low strength (0.30), 30 steps&lt;&#x2F;strong&gt; — faithful, no melt, but a touch soft.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ControlNet-Canny + SDXL&lt;&#x2F;strong&gt; — locks the real edges so nothing warps, but recomposed the scene greener and more stylised than the source.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RealVisXL&lt;&#x2F;strong&gt; (a photoreal SDXL fine-tune), img2img — the winner: sharp, photoreal, keeps the real POV and palette, no melt.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;ai-broll&#x2F;realvis-match.avif&quot; alt=&quot;A real frame and its RealVisXL img2img regeneration. Same POV, mirrors, rack and trail; cleaner and sharper.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The lesson: for footage-matching, the &lt;em&gt;base model&lt;&#x2F;em&gt; and &lt;em&gt;denoising strength&lt;&#x2F;em&gt; matter far more than the prompt. A photoreal checkpoint at low strength beats a flashy distilled model every time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-ai-ken-burns-depth-parallax&quot;&gt;3. AI Ken Burns — depth parallax&lt;&#x2F;h2&gt;
&lt;p&gt;A still doesn’t have to stay still. &lt;strong&gt;Depth-Anything-V2&lt;&#x2F;strong&gt; estimates a depth map for a frame in well under a second; with depth in hand you can render a 2.5D camera move where near pixels shift more than far ones, plus a gentle push toward the trail’s vanishing point. It reads like riding forward.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;ai-broll&#x2F;depth-parallax.avif&quot; alt=&quot;A still and its estimated depth map from Depth-Anything-V2. Near = bright, far = dark; the trail recedes cleanly.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The whole render is a backward &lt;code&gt;cv2.remap&lt;&#x2F;code&gt; with depth-weighted displacement, so it’s hole-free and fast. One catch worth writing down: OpenCV’s &lt;code&gt;remap&lt;&#x2F;code&gt; wants &lt;code&gt;float32&lt;&#x2F;code&gt; maps, and a stray Python scalar in the arithmetic silently upcasts them to &lt;code&gt;float64&lt;&#x2F;code&gt;, at which point &lt;code&gt;remap&lt;&#x2F;code&gt; asserts. Cast the maps back to &lt;code&gt;float32&lt;&#x2F;code&gt; explicitly.&lt;&#x2F;p&gt;
&lt;p&gt;This is the best value-for-effort of the synthetic options: real content, real-looking motion, a tiny model. The only limit is that large pushes start to reveal warping at depth discontinuities — keep the move subtle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-generative-video-stable-video-diffusion&quot;&gt;4. Generative video — Stable Video Diffusion&lt;&#x2F;h2&gt;
&lt;p&gt;If you want motion a static frame genuinely &lt;em&gt;cannot&lt;&#x2F;em&gt; fake, &lt;strong&gt;Stable Video Diffusion&lt;&#x2F;strong&gt; (image-to-video) animates a still into real movement — the camera drifts and pushes through the scene, parallax and all, invented by the model rather than warped geometrically.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;ai-broll&#x2F;svd-motion.avif&quot; alt=&quot;A frame from a Stable Video Diffusion clip generated from a RealVisXL still — the camera has moved through the scene, coherently.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Seeding SVD from the RealVisXL stills gives generated motion that still looks like the ride. The honest caveats: clips are short (~2–3 seconds at 25 frames), the motion is only loosely controllable (you nudge it with a “motion bucket”, not a path), and there’s mild edge warping. It’s also the heaviest thing here — the model is ~9 GB and, on a 12 GB card, only fits with CPU-offload, UNet forward-chunking and a small VAE decode chunk:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;pipe.enable_model_cpu_offload()
pipe.unet.enable_forward_chunking()
frames = pipe(image, num_frames=25, decode_chunk_size=2,
              motion_bucket_id=110, noise_aug_strength=0.04).frames[0]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;5-cinematic-slow-motion-real-footage-no-ai&quot;&gt;5. Cinematic slow-motion — real footage, no AI&lt;&#x2F;h2&gt;
&lt;p&gt;The fifth option uses no generative model at all. Take the auto-mined moments, slow them to 50% with &lt;strong&gt;motion-compensated optical-flow interpolation&lt;&#x2F;strong&gt; (so the slow-mo is smooth, not stuttery duplicated frames), stabilise lightly, and apply a filmic grade. ffmpeg does it in one filter chain:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;minterpolate=mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1:fps=60,setpts=2.0*PTS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It’s 100% real, looks premium, and is the most “broadcast” of the lot. Interpolation can smear very fast edges, but on scenic cutaways it’s clean.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-one-wins&quot;&gt;Which one wins?&lt;&#x2F;h2&gt;
&lt;p&gt;None — they’re complementary, and a real edit blends several:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;&#x2F;th&gt;&lt;th&gt;Authenticity&lt;&#x2F;th&gt;&lt;th&gt;Control&lt;&#x2F;th&gt;&lt;th&gt;Cost&lt;&#x2F;th&gt;&lt;th&gt;Best for&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 · Auto-mine (CV)&lt;&#x2F;td&gt;&lt;td&gt;Real&lt;&#x2F;td&gt;&lt;td&gt;Limited to what you shot&lt;&#x2F;td&gt;&lt;td&gt;Instant&lt;&#x2F;td&gt;&lt;td&gt;Authentic cutaways, free&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2 · Generative stills (RealVisXL)&lt;&#x2F;td&gt;&lt;td&gt;Synthetic&lt;&#x2F;td&gt;&lt;td&gt;High (any scene)&lt;&#x2F;td&gt;&lt;td&gt;Medium (GPU)&lt;&#x2F;td&gt;&lt;td&gt;Shots you never filmed&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3 · Depth parallax&lt;&#x2F;td&gt;&lt;td&gt;Real + fake motion&lt;&#x2F;td&gt;&lt;td&gt;Motion only&lt;&#x2F;td&gt;&lt;td&gt;Cheap&lt;&#x2F;td&gt;&lt;td&gt;Bringing a great frame alive&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;4 · SVD video&lt;&#x2F;td&gt;&lt;td&gt;Synthetic&lt;&#x2F;td&gt;&lt;td&gt;Loose motion&lt;&#x2F;td&gt;&lt;td&gt;Heaviest (~9 GB)&lt;&#x2F;td&gt;&lt;td&gt;Living motion with no footage&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;5 · Slow-mo (real)&lt;&#x2F;td&gt;&lt;td&gt;100% real&lt;&#x2F;td&gt;&lt;td&gt;Speed&#x2F;grade&lt;&#x2F;td&gt;&lt;td&gt;Medium&lt;&#x2F;td&gt;&lt;td&gt;A premium cut of real moments&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;If I had to ship one b-roll bed for this kind of footage, it’d be &lt;strong&gt;slow-mo (5) for the authentic premium feel&lt;&#x2F;strong&gt;, with &lt;strong&gt;depth parallax (3)&lt;&#x2F;strong&gt; to add motion to standout frames, and &lt;strong&gt;RealVisXL stills (2)&lt;&#x2F;strong&gt; only where I need an establishing shot that was never possible to film.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stack&quot;&gt;Stack&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU:&lt;&#x2F;strong&gt; one &lt;strong&gt;RTX 4070 Ti, 12 GB&lt;&#x2F;strong&gt;. Everything below fits.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Diffusion:&lt;&#x2F;strong&gt; &lt;code&gt;diffusers&lt;&#x2F;code&gt; 0.38 — SDXL-base, &lt;strong&gt;RealVisXL V4.0&lt;&#x2F;strong&gt;, ControlNet-Canny-SDXL, &lt;strong&gt;Stable Video Diffusion XT&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Depth:&lt;&#x2F;strong&gt; &lt;code&gt;transformers&lt;&#x2F;code&gt; + &lt;strong&gt;Depth-Anything-V2-Small&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CV &#x2F; assembly:&lt;&#x2F;strong&gt; OpenCV, NumPy, and &lt;strong&gt;ffmpeg&lt;&#x2F;strong&gt; (&lt;code&gt;minterpolate&lt;&#x2F;code&gt;, &lt;code&gt;deshake&lt;&#x2F;code&gt;, AV1 NVENC, AVIF stills).&lt;&#x2F;li&gt;
&lt;li&gt;A couple of traps worth keeping: a distro Python marked &lt;strong&gt;PEP 668 externally-managed&lt;&#x2F;strong&gt; will &lt;em&gt;silently&lt;&#x2F;em&gt; refuse &lt;code&gt;pip install&lt;&#x2F;code&gt; — use a real virtualenv. And &lt;code&gt;pgrep -f build.sh&lt;&#x2F;code&gt; happily matches the &lt;em&gt;watcher command&lt;&#x2F;em&gt; that contains that string, so don’t trust it to tell you a render finished; check for a live &lt;code&gt;ffmpeg&lt;&#x2F;code&gt; instead.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;why-it-exists&quot;&gt;Why it exists&lt;&#x2F;h2&gt;
&lt;p&gt;This started as “the onboard clips need cutaways and I have one camera.” The honest answer turned out to be that you don’t need a second camera — you need to &lt;em&gt;find&lt;&#x2F;em&gt; the b-roll you already shot (1, 5) and &lt;em&gt;fabricate&lt;&#x2F;em&gt; the rest with the smallest model that does the job (3 before 2 before 4). The flashiest option (generative video) is the one I’d reach for last. The least flashy (scoring frames with a Laplacian) is the one I’d reach for first.&lt;&#x2F;p&gt;
&lt;p&gt;All local, all on a card that costs less than a weekend of cloud GPU. Not interesting until you need it — and then it’s exactly what you need.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Stitching drone panoramas: OpenCV vs Hugin, benchmarked</title>
        <published>2026-05-31T00:00:00+00:00</published>
        <updated>2026-05-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/drone-panorama-stitching-opencv-vs-hugin/"/>
        <id>https://www.x2q.net/post/drone-panorama-stitching-opencv-vs-hugin/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/drone-panorama-stitching-opencv-vs-hugin/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; I had a folder of 63 DJI Phantom 4 Pro frames shot over Randers and wanted panoramas out of them. I stitched the same sweeps two ways: &lt;strong&gt;OpenCV’s &lt;code&gt;Stitcher&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; (fully automatic, spherical) and the &lt;strong&gt;Hugin command-line pipeline&lt;&#x2F;strong&gt; (&lt;code&gt;cpfind&lt;&#x2F;code&gt; → &lt;code&gt;autooptimiser&lt;&#x2F;code&gt; → &lt;code&gt;nona&lt;&#x2F;code&gt; → &lt;code&gt;enblend&lt;&#x2F;code&gt;). OpenCV is fast (a few seconds) and keeps the widest field of view, but the horizon comes out wavy, the borders are ragged, and exposure seams are visible. Hugin takes ~30 s per panorama but gives a &lt;strong&gt;straight horizon, seamless multi-band blend, and a clean rectangular crop&lt;&#x2F;strong&gt;. For anything you’d print or publish, Hugin wins. Full commands and a benchmark table below.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-starting-point&quot;&gt;The starting point&lt;&#x2F;h2&gt;
&lt;p&gt;A folder, 63 files, &lt;code&gt;DJI_0029.jpeg&lt;&#x2F;code&gt; through &lt;code&gt;DJI_0091.jpeg&lt;&#x2F;code&gt;, ~15 MB each, 5464 × 3070 px. No flight log, no project file, and — as I’d find out — most of the useful metadata stripped. Just JPEGs and a stray &lt;code&gt;result.jpg&lt;&#x2F;code&gt; from someone’s earlier attempt.&lt;&#x2F;p&gt;
&lt;p&gt;First job: figure out &lt;em&gt;what these photos actually are&lt;&#x2F;em&gt; before deciding how to stitch them. Are they a nadir mapping grid? A single rotational panorama? Several separate sweeps? You stitch each of those differently.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reverse-engineering-the-capture-from-metadata&quot;&gt;Reverse-engineering the capture from metadata&lt;&#x2F;h2&gt;
&lt;p&gt;No &lt;code&gt;exiftool&lt;&#x2F;code&gt; on the box, so:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo apt-get install -y libimage-exiftool-perl imagemagick
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;DJI normally writes gimbal yaw&#x2F;pitch&#x2F;roll into an XMP namespace, which would tell me exactly how the camera was pointing for each frame. Here it had been stripped — the files had clearly been re-exported at some point (which also explains that root-owned &lt;code&gt;result.jpg&lt;&#x2F;code&gt;). What survived was GPS and timestamps:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;exiftool -n -T -FileName -GPSLatitude -GPSLongitude -GPSAltitude \
  -SubSecDateTimeOriginal DJI_*.jpeg
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Two things fell out immediately.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;There were two sessions, 90 minutes apart:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0029&lt;&#x2F;code&gt;–&lt;code&gt;0054&lt;&#x2F;code&gt;, shot 09:01–09:15&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;0055&lt;&#x2F;code&gt;–&lt;code&gt;0091&lt;&#x2F;code&gt;, shot 10:31–10:36&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;The second session was a stack of horizontal sweeps at rising altitude.&lt;&#x2F;strong&gt; The GPS position barely moved while the altitude climbed in clear bands — 169 m, then 221 m, then 240 m, then 269 m. That’s the signature of a drone hovering on the spot and panning the camera across the horizon at each height. Each altitude band is one panorama.&lt;&#x2F;p&gt;
&lt;p&gt;A quick contact sheet confirmed it:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;ls DJI_*.jpeg | sed &amp;#39;s&#x2F;.jpeg&#x2F;&#x2F;&amp;#39; | xargs -P4 -I{} \
  convert {}.jpeg -resize 260x -gravity South -background black \
  -splice 0x16 -pointsize 13 -fill yellow -annotate +0+1 &amp;#39;{}&amp;#39; &#x2F;tmp&#x2F;thumbs&#x2F;{}.png
montage &#x2F;tmp&#x2F;thumbs&#x2F;DJI_00{55..91}.png -tile 6x -geometry +3+3 &#x2F;tmp&#x2F;sheet.png
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Session one turned out to be mixed — top-down shots of a single property at varying heights (a vertical zoom sequence, not a panorama) plus some oblique neighbourhood passes. So I focused the panoramas on session two’s sweeps, which is what the drone was clearly there to capture.&lt;&#x2F;p&gt;
&lt;p&gt;The groups I settled on:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Group&lt;&#x2F;th&gt;&lt;th&gt;Frames&lt;&#x2F;th&gt;&lt;th&gt;Altitude&lt;&#x2F;th&gt;&lt;th&gt;Content&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;A&lt;&#x2F;td&gt;&lt;td&gt;0055–0065&lt;&#x2F;td&gt;&lt;td&gt;~169 m&lt;&#x2F;td&gt;&lt;td&gt;Residential, low pan&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;B&lt;&#x2F;td&gt;&lt;td&gt;0066–0076&lt;&#x2F;td&gt;&lt;td&gt;~221 m&lt;&#x2F;td&gt;&lt;td&gt;Town&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C+D&lt;&#x2F;td&gt;&lt;td&gt;0077–0091&lt;&#x2F;td&gt;&lt;td&gt;240–269 m&lt;&#x2F;td&gt;&lt;td&gt;High skyline&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;technique-1-opencv-stitcher&quot;&gt;Technique 1 — OpenCV Stitcher&lt;&#x2F;h2&gt;
&lt;p&gt;The path of least resistance. OpenCV ships a high-level &lt;code&gt;Stitcher&lt;&#x2F;code&gt; that does feature detection, matching, bundle adjustment, warping, and blending in one call.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;pip install opencv-contrib-python numpy
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;python&quot;&gt;import cv2, glob

imgs = [cv2.imread(f) for f in sorted(glob.glob(&amp;quot;group_A&#x2F;*.jpg&amp;quot;))]
stitcher = cv2.Stitcher_create(cv2.Stitcher_PANORAMA)   # spherical model
status, pano = stitcher.stitch(imgs)
if status == cv2.Stitcher_OK:
    cv2.imwrite(&amp;quot;A_opencv.jpg&amp;quot;, pano)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Two modes matter:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PANORAMA&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; assumes the camera rotated about its optical centre (spherical warp). Correct for these hover-and-pan sweeps.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SCANS&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; assumes an affine&#x2F;translational model (flatbed scans, nadir mapping). I tried it for completeness; it’s the wrong model here and collapsed most sweeps down to two or three frames. Don’t use it for rotational aerials.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I ran everything on a 2400 px working set first — full-res stitching is slow to iterate on, and you want to see whether a group connects before you commit minutes to it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Verdict on OpenCV:&lt;&#x2F;strong&gt; it just works, with zero parameters, in seconds. Group A — eleven frames — stitched into a 10433 × 1595 panorama in about 5 seconds. The catch is the output: a &lt;strong&gt;wavy horizon&lt;&#x2F;strong&gt;, &lt;strong&gt;ragged torn edges&lt;&#x2F;strong&gt; you have to crop by hand, and &lt;strong&gt;faint exposure seams&lt;&#x2F;strong&gt; where frames meet. Fine for a quick look; not something you’d print.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;technique-2-the-hugin-pipeline&quot;&gt;Technique 2 — the Hugin pipeline&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hugin.sourceforge.io&#x2F;&quot;&gt;Hugin&lt;&#x2F;a&gt; is the open-source panorama tool, and crucially its whole engine is scriptable from the command line — no GUI needed.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo apt-get install -y hugin-tools enblend enfuse
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The pipeline, stage by stage:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;pto_gen      -o p.pto  DJI_0055.jpeg ... DJI_0065.jpeg   # 1. project from EXIF
cpfind --multirow -o p.pto p.pto                          # 2. find control points
cpclean      -o p.pto p.pto                               # 3. drop bad matches
autooptimiser -a -m -l -s -o p.pto p.pto                  # 4. optimise + level horizon
pano_modify --canvas=AUTO --crop=AUTO --projection=2 \
             -o p.pto p.pto                               # 5. cylindrical + auto-crop
nona  -m TIFF_m -z LZW -o remap p.pto                     # 6. remap each frame
enblend -o pano.tif remap*.tif                            # 7. multi-band blend
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What each stage buys you over the OpenCV one-liner:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cpfind --multirow&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; finds proper control points across the whole set — it reported 250, 302, and 718 points for groups A, B and C+D respectively.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;autooptimiser -l -s&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; levels the horizon and straightens the panorama. This is the single biggest visible win: the wave disappears.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--projection=2&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is cylindrical, which keeps the horizon a straight horizontal line — exactly what you want for a wide aerial sweep.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;--crop=AUTO&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; trims to the largest clean rectangle, so no manual cropping.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;enblend&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; does multi-band (Burt–Adelson) blending, which hides the exposure differences between frames that OpenCV left visible.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Same group, two pipelines, stacked — OpenCV on top, Hugin below:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;drone-panorama&#x2F;compare-cv-vs-hugin.avif&quot; alt=&quot;OpenCV Stitcher vs Hugin on the same town sweep. OpenCV leaves a wavy horizon and torn edges; Hugin produces a level, seamless rectangle.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The difference isn’t subtle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-benchmark&quot;&gt;The benchmark&lt;&#x2F;h2&gt;
&lt;p&gt;Same input groups, both pipelines. OpenCV numbers are the 2400 px working set; Hugin numbers are the final full-resolution 5464 px runs.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Group&lt;&#x2F;th&gt;&lt;th&gt;Imgs&lt;&#x2F;th&gt;&lt;th&gt;Tool&lt;&#x2F;th&gt;&lt;th&gt;Result&lt;&#x2F;th&gt;&lt;th&gt;Time&lt;&#x2F;th&gt;&lt;th&gt;Horizon&lt;&#x2F;th&gt;&lt;th&gt;Blend&lt;&#x2F;th&gt;&lt;th&gt;Crop&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;A · residential&lt;&#x2F;td&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;OpenCV PANORAMA&lt;&#x2F;td&gt;&lt;td&gt;10433 × 1595&lt;&#x2F;td&gt;&lt;td&gt;5 s&lt;&#x2F;td&gt;&lt;td&gt;wavy&lt;&#x2F;td&gt;&lt;td&gt;seams&lt;&#x2F;td&gt;&lt;td&gt;ragged&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;A · residential&lt;&#x2F;td&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Hugin + enblend&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;11581 × 1827&lt;&#x2F;td&gt;&lt;td&gt;31 s&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;straight&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;seamless&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;clean&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;B · town&lt;&#x2F;td&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;OpenCV PANORAMA&lt;&#x2F;td&gt;&lt;td&gt;10295 × 1838&lt;&#x2F;td&gt;&lt;td&gt;11 s&lt;&#x2F;td&gt;&lt;td&gt;wavy&lt;&#x2F;td&gt;&lt;td&gt;seams&lt;&#x2F;td&gt;&lt;td&gt;ragged&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;B · town&lt;&#x2F;td&gt;&lt;td&gt;11&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Hugin + enblend&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;8190 × 2547&lt;&#x2F;td&gt;&lt;td&gt;30 s&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;straight&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;seamless&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;clean&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C+D · skyline&lt;&#x2F;td&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;OpenCV PANORAMA&lt;&#x2F;td&gt;&lt;td&gt;8660 × 1975&lt;&#x2F;td&gt;&lt;td&gt;2 s&lt;&#x2F;td&gt;&lt;td&gt;wavy&lt;&#x2F;td&gt;&lt;td&gt;seams&lt;&#x2F;td&gt;&lt;td&gt;ragged&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;C+D · skyline&lt;&#x2F;td&gt;&lt;td&gt;15&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;Hugin + enblend&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;22469 × 2391&lt;&#x2F;td&gt;&lt;td&gt;60 s&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;straight&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;seamless&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;&lt;strong&gt;clean&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;OpenCV is 3–10× faster and tends to keep a wider field of view (it warps and keeps the torn edges rather than cropping them). Hugin is slower but wins on every quality axis that matters for a finished image.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;going-full-resolution&quot;&gt;Going full resolution&lt;&#x2F;h2&gt;
&lt;p&gt;Once Hugin was the clear winner I re-ran it on the original 5464 px frames. The pipeline is identical — just point &lt;code&gt;pto_gen&lt;&#x2F;code&gt; at the original JPEGs instead of the downscaled set. &lt;code&gt;cpfind&lt;&#x2F;code&gt; and &lt;code&gt;enblend&lt;&#x2F;code&gt; are the slow stages at full res, but it still came in around 30 s per panorama, a minute for the 15-frame skyline.&lt;&#x2F;p&gt;
&lt;p&gt;The finished panoramas:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The high skyline — fifteen frames, 22469 × 2391, a near-180° sweep across the whole town:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;drone-panorama&#x2F;skyline-hugin.avif&quot; alt=&quot;Randers skyline panorama, full sweep, stitched with Hugin and enblend.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The residential sweep at 169 m:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;drone-panorama&#x2F;residential-hugin.avif&quot; alt=&quot;Residential panorama at 169 m, Hugin.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The town at 221 m:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;img&#x2F;drone-panorama&#x2F;town-hugin.avif&quot; alt=&quot;Town panorama at 221 m, Hugin.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;At 22469 px wide the skyline is comfortably large-format printable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;why-did-the-metadata-matter-so-much-why-not-just-throw-all-63-into-the-stitcher&quot;&gt;Why did the metadata matter so much — why not just throw all 63 into the stitcher?&lt;&#x2F;h3&gt;
&lt;p&gt;You can, and OpenCV will try to find connected components. But the files were two unrelated sessions plus a mix of nadir and oblique frames. Feeding the lot in risks the optimiser bridging frames that shouldn’t connect, or wasting minutes failing to. Five minutes with &lt;code&gt;exiftool&lt;&#x2F;code&gt; to group the frames first saved a lot of guessing — and told me &lt;em&gt;which&lt;&#x2F;em&gt; groups were even panoramas.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-not-just-use-the-drone-s-built-in-panorama-mode&quot;&gt;Why not just use the drone’s built-in panorama mode?&lt;&#x2F;h3&gt;
&lt;p&gt;These weren’t shot as in-camera panoramas — they’re individual frames from manual sweeps, and the on-board stitch (if it ran at all) is the low-res &lt;code&gt;result.jpg&lt;&#x2F;code&gt; I found in the folder. Stitching from the original full-res frames yields far more detail than the in-camera JPEG.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-about-ptgui-lightroom-microsoft-ice&quot;&gt;What about PTGui &#x2F; Lightroom &#x2F; Microsoft ICE?&lt;&#x2F;h3&gt;
&lt;p&gt;All capable. PTGui is essentially commercial Hugin and excellent. Lightroom’s Photo Merge is convenient if you’re already in it. Microsoft ICE was great but is discontinued. I wanted a scriptable, free, Linux-native pipeline I could run headless over a folder — that’s Hugin.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cylindrical-spherical-or-rectilinear&quot;&gt;Cylindrical, spherical, or rectilinear?&lt;&#x2F;h3&gt;
&lt;p&gt;For a wide horizontal sweep, &lt;strong&gt;cylindrical&lt;&#x2F;strong&gt; (&lt;code&gt;--projection=2&lt;&#x2F;code&gt;) keeps the horizon straight and handles &amp;gt;120° without the extreme stretching rectilinear gives at the edges. Spherical (what OpenCV’s &lt;code&gt;PANORAMA&lt;&#x2F;code&gt; uses) is fine too but tends to bow the horizon unless the pitch is well estimated — which is harder here because the gimbal angles were stripped.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-this-run-unattended-over-a-whole-folder&quot;&gt;Can this run unattended over a whole folder?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — the entire Hugin path is CLI, so it drops straight into a shell script or a &lt;code&gt;Makefile&lt;&#x2F;code&gt;. Group the frames (by timestamp&#x2F;altitude, or just let &lt;code&gt;cpfind&lt;&#x2F;code&gt; find connected components), then loop the seven commands per group. That’s the real argument for Hugin over a GUI tool here.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;&#x2F;h2&gt;
&lt;p&gt;If you want a panorama &lt;em&gt;now&lt;&#x2F;em&gt; and don’t care about a wavy horizon or trimming edges yourself, OpenCV’s &lt;code&gt;Stitcher&lt;&#x2F;code&gt; is a five-second one-liner and genuinely impressive for the effort.&lt;&#x2F;p&gt;
&lt;p&gt;For anything you’ll keep — print, publish, hang on a wall — the &lt;strong&gt;Hugin command-line pipeline&lt;&#x2F;strong&gt; is worth the extra half-minute. A straight horizon, a seamless blend, and an automatic clean crop are exactly the things that separate a snapshot from a finished panorama, and Hugin gets all three for free. It’s now my default for stitching anything off the drone.&lt;&#x2F;p&gt;
&lt;p&gt;The whole thing — install to finished full-res panoramas — was about an hour, most of it spent understanding the photos rather than stitching them. Which is usually how these go.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>bchunk — convert .bin&#x2F;.cue images to .iso and .wav on Linux and macOS</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/bchunk-bin-cue-to-iso-wav/"/>
        <id>https://www.x2q.net/post/bchunk-bin-cue-to-iso-wav/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/bchunk-bin-cue-to-iso-wav/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — sometimes called &lt;strong&gt;&lt;code&gt;binchunker&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — is a small, old, Unix command-line tool that takes a &lt;code&gt;.bin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.cue&lt;&#x2F;code&gt; pair (a raw CD image with a cue sheet) and splits it into a proper &lt;strong&gt;&lt;code&gt;.iso&lt;&#x2F;code&gt; file for the data track&lt;&#x2F;strong&gt; and one &lt;strong&gt;&lt;code&gt;.wav&lt;&#x2F;code&gt; file per audio track&lt;&#x2F;strong&gt;. It’s the right tool when a download or archive gave you &lt;code&gt;game.bin&lt;&#x2F;code&gt; + &lt;code&gt;game.cue&lt;&#x2F;code&gt; and you actually want to &lt;strong&gt;mount the data track&lt;&#x2F;strong&gt; or &lt;strong&gt;re-burn the audio tracks&lt;&#x2F;strong&gt;. Install with &lt;code&gt;apt install bchunk&lt;&#x2F;code&gt; on Debian&#x2F;Ubuntu, &lt;code&gt;brew install bchunk&lt;&#x2F;code&gt; on macOS, or build from source on anything else. Usage is &lt;code&gt;bchunk [options] image.bin image.cue basename&lt;&#x2F;code&gt; — you get &lt;code&gt;basename01.iso&lt;&#x2F;code&gt;, &lt;code&gt;basename02.wav&lt;&#x2F;code&gt;, &lt;code&gt;basename03.wav&lt;&#x2F;code&gt;, and so on.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-bin-cue-is-and-why-you-d-want-to-split-it&quot;&gt;What .bin&#x2F;.cue is, and why you’d want to split it&lt;&#x2F;h2&gt;
&lt;p&gt;A &lt;code&gt;.bin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.cue&lt;&#x2F;code&gt; pair is a &lt;strong&gt;raw, bit-accurate CD image&lt;&#x2F;strong&gt; plus a plain-text sheet that describes the track layout. The &lt;code&gt;.bin&lt;&#x2F;code&gt; contains every sector of the CD back-to-back — including data tracks (Mode 1 or Mode 2), audio tracks (raw Red Book PCM, 2352 bytes per sector, no filesystem), and the gaps between them. The &lt;code&gt;.cue&lt;&#x2F;code&gt; tells you where each track starts and what type it is.&lt;&#x2F;p&gt;
&lt;p&gt;This format is the standard output of CD-ripping tools like cdrdao and of old game-preservation scrapes, because it preserves &lt;em&gt;everything&lt;&#x2F;em&gt; — pregaps, CD-TEXT, mixed-mode CDs, CD-DA audio. That’s also why you can’t just rename &lt;code&gt;game.bin&lt;&#x2F;code&gt; to &lt;code&gt;game.iso&lt;&#x2F;code&gt; and mount it: a mixed-mode CD has &lt;strong&gt;non-filesystem sectors&lt;&#x2F;strong&gt; at the beginning, audio tracks that aren’t a filesystem at all, and per-sector headers that ISO expects stripped.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt; does the conversion properly:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data tracks&lt;&#x2F;strong&gt; become ISO 9660 files (&lt;code&gt;.iso&lt;&#x2F;code&gt;) — the 2352-byte CD sectors are trimmed to 2048-byte ISO sectors, headers discarded.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Audio tracks&lt;&#x2F;strong&gt; become 16-bit stereo 44.1 kHz &lt;code&gt;.wav&lt;&#x2F;code&gt; files, each with a proper WAV header.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;After that you can &lt;code&gt;mount -o loop&lt;&#x2F;code&gt; the &lt;code&gt;.iso&lt;&#x2F;code&gt;, play the &lt;code&gt;.wav&lt;&#x2F;code&gt;s, or feed them to a burner.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;installing&quot;&gt;Installing&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;debian-ubuntu-mint&quot;&gt;Debian &#x2F; Ubuntu &#x2F; Mint&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install bchunk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;macos-homebrew&quot;&gt;macOS (Homebrew)&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;brew install bchunk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;arch-manjaro&quot;&gt;Arch &#x2F; Manjaro&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S bchunk
# or from AUR: yay -S bchunk
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;fedora-rhel-centos&quot;&gt;Fedora &#x2F; RHEL &#x2F; CentOS&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt; isn’t in the default repos; get it from RPMFusion or build from source:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;git clone https:&#x2F;&#x2F;github.com&#x2F;hessu&#x2F;bchunk.git
cd bchunk
make
sudo cp bchunk &#x2F;usr&#x2F;local&#x2F;bin&#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;windows&quot;&gt;Windows&lt;&#x2F;h3&gt;
&lt;p&gt;Use WSL and &lt;code&gt;apt install bchunk&lt;&#x2F;code&gt;. There’s an old native port but WSL is easier.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;basic-usage&quot;&gt;Basic usage&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;bchunk game.bin game.cue game
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Output:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;game01.iso    # data track, mountable as ISO 9660
game02.wav    # audio track 1
game03.wav    # audio track 2
...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;basename&lt;&#x2F;code&gt; (&lt;code&gt;game&lt;&#x2F;code&gt; in the example) is the prefix for every output file. Track numbers start at &lt;code&gt;01&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;useful-options&quot;&gt;Useful options&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt; has a small, stable set of flags — most of them are there to handle specific CD quirks.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-v&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — verbose. Prints each track as it’s processed. Use this the first time you run it to confirm you’re getting the track layout you expect.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-w&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — write &lt;code&gt;.wav&lt;&#x2F;code&gt; files (default). Explicit form.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-r&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — raw audio output instead of &lt;code&gt;.wav&lt;&#x2F;code&gt;. Useful if you’re piping into &lt;code&gt;sox&lt;&#x2F;code&gt; or &lt;code&gt;ffmpeg&lt;&#x2F;code&gt; and don’t want a second header-stripping step.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-p&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — &lt;strong&gt;Psx&#x2F;PlayStation mode&lt;&#x2F;strong&gt;. Treats Mode 2 sectors more loosely — needed for many PlayStation CD images where the cue sheet is imprecise.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-s&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — swap byte order on audio tracks. Needed if the &lt;code&gt;.bin&lt;&#x2F;code&gt; was produced by a ripper that wrote little-endian audio where &lt;code&gt;bchunk&lt;&#x2F;code&gt; expects big-endian (or vice versa). Symptom: output &lt;code&gt;.wav&lt;&#x2F;code&gt; sounds like white noise.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-W&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — write &lt;code&gt;.wav&lt;&#x2F;code&gt; with a verified (rather than computed) header size. Rarely needed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-E&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — use the exact track boundaries from the cue sheet rather than probing. Use this if audio tracks sound clipped at the beginning or end.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mounting-the-resulting-iso&quot;&gt;Mounting the resulting .iso&lt;&#x2F;h2&gt;
&lt;p&gt;On Linux:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir &#x2F;mnt&#x2F;cd
sudo mount -o loop game01.iso &#x2F;mnt&#x2F;cd
ls &#x2F;mnt&#x2F;cd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On macOS:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;hdiutil attach game01.iso
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;common-gotchas&quot;&gt;Common gotchas&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;illegal-or-unsupported-track-type-or-empty-output&quot;&gt;“Illegal or unsupported track type” or empty output&lt;&#x2F;h3&gt;
&lt;p&gt;Your cue sheet is malformed or references a &lt;code&gt;.bin&lt;&#x2F;code&gt; size that doesn’t match. Open the &lt;code&gt;.cue&lt;&#x2F;code&gt; in a text editor — it’s plain text — and check:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;FILE &quot;...&quot;&lt;&#x2F;code&gt; line points at the right &lt;code&gt;.bin&lt;&#x2F;code&gt; filename, case-sensitive.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;TRACK&lt;&#x2F;code&gt; entries are numbered sequentially with types &lt;code&gt;MODE1&#x2F;2352&lt;&#x2F;code&gt;, &lt;code&gt;MODE2&#x2F;2352&lt;&#x2F;code&gt;, or &lt;code&gt;AUDIO&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Index entries (&lt;code&gt;INDEX 00&lt;&#x2F;code&gt;, &lt;code&gt;INDEX 01&lt;&#x2F;code&gt;) are MM:SS:FF, with FF being frames (0–74).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;audio-tracks-sound-like-static&quot;&gt;Audio tracks sound like static&lt;&#x2F;h3&gt;
&lt;p&gt;Byte order mismatch. Retry with &lt;code&gt;-s&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bchunk -s game.bin game.cue game
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;single-iso-is-fine-but-audio-tracks-are-silent-or-truncated&quot;&gt;Single &lt;code&gt;.iso&lt;&#x2F;code&gt; is fine, but audio tracks are silent or truncated&lt;&#x2F;h3&gt;
&lt;p&gt;Try &lt;code&gt;-E&lt;&#x2F;code&gt; to force cue-sheet-exact boundaries:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bchunk -E game.bin game.cue game
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;playstation-psx-image-won-t-split-cleanly&quot;&gt;PlayStation&#x2F;PSX image won’t split cleanly&lt;&#x2F;h3&gt;
&lt;p&gt;Use &lt;code&gt;-p&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bchunk -p psx.bin psx.cue psx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the most common complaint on forums — PSX cues are almost always Mode 2 and &lt;code&gt;bchunk&lt;&#x2F;code&gt; without &lt;code&gt;-p&lt;&#x2F;code&gt; is strict about them.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;multiple-bin-files-referenced-in-the-cue&quot;&gt;Multiple &lt;code&gt;.bin&lt;&#x2F;code&gt; files referenced in the cue&lt;&#x2F;h3&gt;
&lt;p&gt;Some rippers produce one &lt;code&gt;.bin&lt;&#x2F;code&gt; per track plus a combining cue sheet. &lt;code&gt;bchunk&lt;&#x2F;code&gt; expects one &lt;code&gt;.bin&lt;&#x2F;code&gt; for the whole disc. Concatenate them first with &lt;code&gt;cat track01.bin track02.bin ... &amp;gt; combined.bin&lt;&#x2F;code&gt; and point a single-file cue at &lt;code&gt;combined.bin&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;alternatives-worth-knowing&quot;&gt;Alternatives worth knowing&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cdemu&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — user-space CD emulator for Linux. Can mount a &lt;code&gt;.bin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.cue&lt;&#x2F;code&gt; directly without splitting. Useful if you just want to &lt;em&gt;read&lt;&#x2F;em&gt; the data track temporarily.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ecm-tools&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — different format (&lt;code&gt;.ecm&lt;&#x2F;code&gt;), but same problem space. Worth knowing if you encounter it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;7z&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;p7zip&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — newer 7-Zip versions can list and extract ISO 9660 data tracks from a raw &lt;code&gt;.bin&lt;&#x2F;code&gt;, but can’t recover audio as &lt;code&gt;.wav&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ffmpeg&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — if you want MP3 &#x2F; FLAC &#x2F; Opus instead of &lt;code&gt;.wav&lt;&#x2F;code&gt;, pipe the raw output: &lt;code&gt;bchunk -r game.bin game.cue game &amp;amp;&amp;amp; ffmpeg -f s16be -ar 44100 -ac 2 -i game02.raw game02.flac&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;why-it-s-still-useful-in-2026&quot;&gt;Why it’s still useful in 2026&lt;&#x2F;h2&gt;
&lt;p&gt;Optical-media imaging has been a solved problem for decades, but &lt;code&gt;.bin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.cue&lt;&#x2F;code&gt; is still the default output of &lt;code&gt;cdrdao&lt;&#x2F;code&gt;, still what old archives are stored as, and still what shows up when someone hands you a CD backup that isn’t an ISO. &lt;code&gt;bchunk&lt;&#x2F;code&gt; is a ~1,000-line C program, written in the late 1990s, that does one job correctly and has barely changed in twenty years. It’s in every major distribution, it compiles cleanly on modern systems, and it’s the right tool.&lt;&#x2F;p&gt;
&lt;p&gt;If you’re trying to recover data from an old CD image: start here.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>binlist.net — how a Fuerteventura holiday side-project became iinlist.com</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/binlist-net-iinlist-com-story/"/>
        <id>https://www.x2q.net/post/binlist-net-iinlist-com-story/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/binlist-net-iinlist-com-story/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;binlist.net&quot;&gt;binlist.net&lt;&#x2F;a&gt; is a &lt;strong&gt;free HTTP API&lt;&#x2F;strong&gt; that takes the first 6–8 digits of a credit card number (the &lt;strong&gt;BIN&lt;&#x2F;strong&gt; or &lt;strong&gt;IIN&lt;&#x2F;strong&gt;) and returns the issuing bank, country, card scheme (Visa, Mastercard, etc.), card type (debit&#x2F;credit&#x2F;prepaid), and category. I started it in &lt;strong&gt;August 2013 on a holiday in Fuerteventura&lt;&#x2F;strong&gt;, because I needed a BIN lookup for another project and everything that existed was either paywalled, stale, or behind a sketchy scraper. It was my &lt;strong&gt;first ever Go project&lt;&#x2F;strong&gt;, hit &lt;strong&gt;100,000 queries&#x2F;day&lt;&#x2F;strong&gt; within weeks, and crossed &lt;strong&gt;1 million queries&#x2F;day in 2015&lt;&#x2F;strong&gt;. It is still online, still free, still &lt;code&gt;curl&lt;&#x2F;code&gt;-friendly. The commercial variant, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;iinlist.com&quot;&gt;iinlist.com&lt;&#x2F;a&gt;, is what I co-founded after binlist.net’s success; it’s the same idea with &lt;strong&gt;payment-industry-grade accuracy&lt;&#x2F;strong&gt;, &lt;strong&gt;8-digit IIN support&lt;&#x2F;strong&gt; (mandated by ISO&#x2F;IEC 7812 since 2022), &lt;strong&gt;ARDEF-backed ranges&lt;&#x2F;strong&gt;, and an SLA — built for banks, PSPs, fraud teams, and issuers who need to treat BIN data as production data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bin-vs-iin-a-one-paragraph-primer&quot;&gt;BIN vs IIN — a one-paragraph primer&lt;&#x2F;h2&gt;
&lt;p&gt;The first six (historically) or eight (since 2017, mandatory 2022) digits of a card number are the &lt;strong&gt;Issuer Identification Number&lt;&#x2F;strong&gt; (IIN). The old name, still widely used, is &lt;strong&gt;Bank Identification Number&lt;&#x2F;strong&gt; (BIN). Given a BIN&#x2F;IIN, you can determine:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scheme &#x2F; network&lt;&#x2F;strong&gt; — Visa, Mastercard, Amex, Discover, JCB, UnionPay, Diners.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Issuer&lt;&#x2F;strong&gt; — the bank or fintech that issued the card.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Country&lt;&#x2F;strong&gt; — country of the issuer, which is not necessarily the cardholder’s country but is a useful proxy for risk scoring, tax logic (EU&#x2F;EEA VAT MOSS), and user experience (preferred language, local payment methods).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Type&lt;&#x2F;strong&gt; — debit, credit, prepaid, deferred debit, charge.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Category &#x2F; product&lt;&#x2F;strong&gt; — Classic, Gold, Platinum, Business, Corporate, etc. Useful for acceptance cost routing (commercial cards cost more to accept).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All of this is “sensitive” in the sense that issuers don’t publish full BIN tables and card schemes consider them proprietary. In practice, BIN ranges leak constantly — every authorisation the scheme routes, every chargeback dispute — and there’s a whole cottage industry around keeping reasonably accurate BIN tables.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-i-built-binlist-net-on-holiday&quot;&gt;Why I built binlist.net on holiday&lt;&#x2F;h2&gt;
&lt;p&gt;Summer 2013. &lt;strong&gt;Playitas, southern Fuerteventura&lt;&#x2F;strong&gt; — the resort on the Atlantic side down by Gran Tarajal. Rented apartment, small balcony, Canary wind doing its thing. I had an idea for a side-project that needed to look up cards by BIN and I did not want to pay a payments vendor €200&#x2F;month for it. The existing free options were:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wikipedia’s IIN list&lt;&#x2F;strong&gt; — reasonably accurate for well-known schemes, woefully incomplete for niche issuers, updated by volunteers who don’t work in payments.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pasted CSVs on forums&lt;&#x2F;strong&gt; — decent coverage for US issuers, always outdated.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Google Fusion Tables&lt;&#x2F;strong&gt; — deprecated by Google that same year.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Scrapers of merchant-bank lookup forms&lt;&#x2F;strong&gt; — ToS-violating, brittle.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;What I wanted was &lt;code&gt;curl http:&#x2F;&#x2F;example.com&#x2F;40012345&lt;&#x2F;code&gt; returning clean JSON. So I built it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-stack-then&quot;&gt;The stack (then)&lt;&#x2F;h3&gt;
&lt;p&gt;This was &lt;strong&gt;my first ever Go project&lt;&#x2F;strong&gt;. I’d been reading about Go for a while and wanted a small, well-scoped excuse to try it; a single-endpoint JSON API over an in-memory BIN table was perfect. Ruby&#x2F;Sinatra would have been faster for me to type, but I wanted to learn something, and the memory and latency characteristics of a Go binary turned out to be exactly what a free-tier public API needed.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go&lt;&#x2F;strong&gt; — a single binary, &lt;code&gt;net&#x2F;http&lt;&#x2F;code&gt;, no framework. The whole server was a few hundred lines.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;In-memory BIN range table&lt;&#x2F;strong&gt;, built at boot from a ~300 MB source file. Lookups were O(log n) over a sorted slice.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deployed on a single Hetzner VM&lt;&#x2F;strong&gt; from day one (I briefly considered Heroku but Heroku’s free dyno memory limits didn’t fit a fully-loaded BIN table).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;JSON + XML + CSV&lt;&#x2F;strong&gt; response formats, because 2013.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Data seed&lt;&#x2F;strong&gt; from a combination of public Wikipedia scrapes, the old “Mars Base” CSV from 2009, and a set of ranges I’d been accumulating at work.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Total build time on that balcony: a weekend — about half of which was me figuring out &lt;code&gt;go build&lt;&#x2F;code&gt;, &lt;code&gt;GOPATH&lt;&#x2F;code&gt; (Go modules didn’t exist yet), and how interfaces worked. I bought the domain on the Monday, deployed the binary, and tweeted about it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;traffic-curve&quot;&gt;Traffic curve&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Week 1:&lt;&#x2F;strong&gt; a few hundred queries&#x2F;day, mostly me testing.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Month 1:&lt;&#x2F;strong&gt; ~&lt;strong&gt;100,000 queries&#x2F;day&lt;&#x2F;strong&gt; after developers on Stack Overflow and a couple of e-commerce forums found it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;By early 2015:&lt;&#x2F;strong&gt; crossed &lt;strong&gt;1 million queries&#x2F;day&lt;&#x2F;strong&gt; sustained, with spikes above 2M during flash-sale events on merchants that called it on every checkout page-load.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Scaling to that point cost almost nothing because Go’s single-binary footprint meant a ~€5&#x2F;month VPS handled the whole thing; the bottleneck for a long time was the NIC, not CPU or memory.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;13-years-later&quot;&gt;13 years later&lt;&#x2F;h3&gt;
&lt;p&gt;binlist.net still runs. It has migrated infrastructure a few times — Hetzner VM → Hetzner fleet → a small fleet behind Cloudflare — and the data seed has been continuously updated, but the API surface (and most of the original Go code) is the same. Today it serves &lt;strong&gt;tens of millions of lookups per month&lt;&#x2F;strong&gt;, free, no API key required, with a soft rate limit on anonymous callers to keep it honest.&lt;&#x2F;p&gt;
&lt;p&gt;It has &lt;strong&gt;~2,000 Google impressions&#x2F;month&lt;&#x2F;strong&gt; for queries like “binlist”, “bin list bank identification number”, “list of bank identification numbers” — still picking up the long tail of developers who type the same question I typed into Google in 2013.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-iinlist-com-exists&quot;&gt;Why iinlist.com exists&lt;&#x2F;h2&gt;
&lt;p&gt;binlist.net’s data is good enough for “what country is this card from” decisions — risk scoring, currency presentation, analytics. It is &lt;strong&gt;not&lt;&#x2F;strong&gt; good enough for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Acceptance-cost routing&lt;&#x2F;strong&gt; where you route commercial-card transactions through a different processor.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Interchange++ modelling&lt;&#x2F;strong&gt; where the margin difference between Classic and Platinum Mastercard is real money.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Scheme compliance&lt;&#x2F;strong&gt; — the ISO&#x2F;IEC 7812 migration from 6-digit BINs to 8-digit IINs, mandated 2022, broke a lot of “I’ll just use the first six digits” logic.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Co-badged cards&lt;&#x2F;strong&gt; (e.g. Dankort + Visa Debit) where two networks both claim the card and the routing decision is business-critical.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARDEF-grade accuracy&lt;&#x2F;strong&gt; — Visa’s Account Range Definition File is the source of truth; you really do want data derived from ARDEF for anything production-grade.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;iinlist.com solves these, commercially. I co-founded it with a small team of people who know payments data professionally. It’s the product binlist.net is the free-tier advertisement for. Some of our customers are names you’d recognise; most are not. All of them eventually reach the same conclusion: &lt;strong&gt;BIN data that’s “mostly right” costs more in wrong decisions than accurate data costs in licence fees&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-iinlist-com-is-specifically&quot;&gt;What iinlist.com is, specifically&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;8-digit IIN support&lt;&#x2F;strong&gt; across all schemes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Daily updates&lt;&#x2F;strong&gt;, derived from ARDEF + scheme feeds + issuer announcements + internal verification.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Richer enrichment&lt;&#x2F;strong&gt;: card type, product category, issuing country, issuer branding, regulatory classifications (commercial vs consumer, prepaid flag, etc.).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SLA and support&lt;&#x2F;strong&gt;. You can file a ticket and it will actually reach a human.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;On-prem &#x2F; self-hosted option&lt;&#x2F;strong&gt; for customers whose compliance doesn’t allow “BIN lookup as a third-party API call in the authorisation path”.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Pricing&lt;&#x2F;strong&gt; that makes sense for both a scrappy fintech (monthly plan) and an established acquirer (annual, unlimited).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-i-just-use-binlist-net-commercially&quot;&gt;Can I just use binlist.net commercially?&lt;&#x2F;h2&gt;
&lt;p&gt;You can use binlist.net however you want; there’s no gate. But:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;There’s no SLA, no uptime guarantee, and no contract. It’s a side project that happens to have stayed up for 13 years.&lt;&#x2F;li&gt;
&lt;li&gt;Rate limits on anonymous traffic will kick in at production volume. Buy a licence from iinlist.com and the problem is solved.&lt;&#x2F;li&gt;
&lt;li&gt;If your regulator asks where your BIN data comes from, “a free API my developer found” is not the answer you want to give.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For anything hobby, demo, or prototype: binlist.net is perfect. For anything where wrong BIN data becomes someone’s KPI: iinlist.com.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-binlist-net-today&quot;&gt;Using binlist.net today&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;$ curl -sH &amp;quot;Accept-Version: 3&amp;quot; https:&#x2F;&#x2F;lookup.binlist.net&#x2F;45717360
{
  &amp;quot;number&amp;quot;: { &amp;quot;length&amp;quot;: 16, &amp;quot;luhn&amp;quot;: true },
  &amp;quot;scheme&amp;quot;: &amp;quot;visa&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;debit&amp;quot;,
  &amp;quot;brand&amp;quot;: &amp;quot;Visa&#x2F;Dankort&amp;quot;,
  &amp;quot;country&amp;quot;: {
    &amp;quot;numeric&amp;quot;: &amp;quot;208&amp;quot;,
    &amp;quot;alpha2&amp;quot;: &amp;quot;DK&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;Denmark&amp;quot;,
    &amp;quot;emoji&amp;quot;: &amp;quot;🇩🇰&amp;quot;,
    &amp;quot;currency&amp;quot;: &amp;quot;DKK&amp;quot;,
    &amp;quot;latitude&amp;quot;: 56,
    &amp;quot;longitude&amp;quot;: 10
  },
  &amp;quot;bank&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;Jyske Bank A&#x2F;S&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;www.jyskebank.dk&amp;quot;,
    &amp;quot;phone&amp;quot;: &amp;quot;+4589893300&amp;quot;,
    &amp;quot;city&amp;quot;: &amp;quot;Silkeborg&amp;quot;
  }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Accept-Version: 3&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — pins to API v3.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Anonymous rate limit&lt;&#x2F;strong&gt; — documented on the site, plenty for casual use.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Zero warranties&lt;&#x2F;strong&gt; — if the response is wrong, submit a correction on the GitHub repo. For commercial guarantees, see iinlist.com.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-bin-iin-data-regulated&quot;&gt;Is BIN &#x2F; IIN data regulated?&lt;&#x2F;h3&gt;
&lt;p&gt;BIN ranges themselves are not personal data under GDPR — a BIN identifies an issuer, not a person. Combining a BIN with a full card number is a different story and falls under PCI-DSS.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-changed-with-the-8-digit-iin-migration&quot;&gt;What changed with the 8-digit IIN migration?&lt;&#x2F;h3&gt;
&lt;p&gt;ISO&#x2F;IEC 7812 formalised 8-digit IINs in 2017 with a hard migration deadline in April 2022 for the major networks. If your code still treats only the first six digits as the issuer identifier, you’re silently misrouting a growing share of traffic — modern Visa and Mastercard issuer ranges are defined at 8 digits.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-not-just-look-it-up-client-side&quot;&gt;Why not just look it up client-side?&lt;&#x2F;h3&gt;
&lt;p&gt;You can, with a static list in the browser. But you’ll accept a trade-off: freshness (the static list will be out of date within weeks) or bundle size (the current BIN table is megabytes uncompressed). A server-side API is the conventional answer.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-binlist-net-log-the-bins-i-query&quot;&gt;Does binlist.net log the BINs I query?&lt;&#x2F;h3&gt;
&lt;p&gt;Aggregate analytics only — enough to detect abuse and rate-limit. Individual queries are not associated with a user. iinlist.com has a stricter no-logging posture for customers where that matters.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-the-source-code-open&quot;&gt;Is the source code open?&lt;&#x2F;h3&gt;
&lt;p&gt;The API surface of binlist.net is on GitHub, with contribution guidelines for data corrections. The data ingestion pipeline is not open-source; it relies on scheme feeds that require licences.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;are-binlist-net-and-iinlist-com-the-same-company&quot;&gt;Are binlist.net and iinlist.com the same company?&lt;&#x2F;h3&gt;
&lt;p&gt;Separate projects, overlapping origins, same operator on my side. binlist.net is a personal free service; iinlist.com is a commercial company with co-founders and employees. The two are operationally independent but share institutional knowledge about BIN data that is not easy to accumulate from scratch.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;thirteen-years-in&quot;&gt;Thirteen years in&lt;&#x2F;h2&gt;
&lt;p&gt;The thing I find most interesting about binlist.net is not the traffic — it’s the &lt;strong&gt;kind of questions&lt;&#x2F;strong&gt; developers are still typing into Google in 2026: “what is a bin list”, “bin number example”, “list of issuer identification numbers”. These are the exact queries I was typing in 2013 on that balcony in Playitas. Payments as an industry keeps adding layers, but the foundational questions don’t change.&lt;&#x2F;p&gt;
&lt;p&gt;If you’re sitting on a beach with a laptop and you have an itch for a side-project that scratches your own back, go build it — and while you’re at it, try the language you’ve been meaning to learn. Sometimes it turns out a lot of people have the same itch, and thirteen years later you have a useful thing to point them at and a working knowledge of Go.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>My CM3588 project — a 4-NVMe ARM64 home NAS on 10 W idle</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/cm3588-home-nas-project/"/>
        <id>https://www.x2q.net/post/cm3588-home-nas-project/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/cm3588-home-nas-project/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.friendlyelec.com&#x2F;wiki&#x2F;index.php&#x2F;CM3588_NAS_Kit&quot;&gt;FriendlyElec CM3588 NAS Kit&lt;&#x2F;a&gt; is an RK3588-based compute module on a carrier board with &lt;strong&gt;four M.2 NVMe slots (PCIe 3.0 x1 each)&lt;&#x2F;strong&gt;, &lt;strong&gt;2.5 GbE&lt;&#x2F;strong&gt;, &lt;strong&gt;2× HDMI out&lt;&#x2F;strong&gt;, &lt;strong&gt;USB 3.0&lt;&#x2F;strong&gt;, and an 8- or 16-core ARMv8 CPU (4× Cortex-A76 + 4× Cortex-A55). Fully loaded with four NVMe drives it idles at about &lt;strong&gt;10 W&lt;&#x2F;strong&gt;, peaks around 20 W under heavy I&#x2F;O, and reads&#x2F;writes an NVMe at roughly 1 GB&#x2F;s (the bus limit, not the drive). My own unit runs Debian on the mainline kernel, with ZFS on root, four 2 TB NVMe drives in a &lt;code&gt;raidz2&lt;&#x2F;code&gt; pool, Samba for LAN shares, and Tailscale for off-LAN access. It has replaced a tower PC running TrueNAS and cut NAS power draw by about 60 W.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-board&quot;&gt;Why this board&lt;&#x2F;h2&gt;
&lt;p&gt;I’d been running a tower PC as a home NAS for years: a second-hand desktop, six 3.5“ spinning disks, and a noisy fan. It worked, but it was using 70–80 W idle and sitting in a cupboard that needed active ventilation. The disks were loud enough that I’d moved them to the far corner of the house.&lt;&#x2F;p&gt;
&lt;p&gt;Three things I wanted from a replacement:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NVMe-only.&lt;&#x2F;strong&gt; Spinning disks are cheaper per terabyte but the noise, vibration, and heat aren’t worth it for a home NAS sized in the single-digit terabytes. 4× 2 TB NVMe is plenty.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARM, for the power draw.&lt;&#x2F;strong&gt; An x86 board with 4× PCIe slots wired to NVMe would pull 30–40 W idle. ARM can do it in 10.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mainline Linux, not a vendor fork.&lt;&#x2F;strong&gt; I want &lt;code&gt;apt-get dist-upgrade&lt;&#x2F;code&gt; to work, &lt;code&gt;zfs&lt;&#x2F;code&gt; to compile from DKMS against a current kernel, and no three-year-old vendor BSP.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The CM3588 NAS Kit hits all three.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hardware&quot;&gt;Hardware&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SoC:&lt;&#x2F;strong&gt; Rockchip RK3588 (4× Cortex-A76 @ 2.4 GHz + 4× Cortex-A55 @ 1.8 GHz, Mali-G610 GPU, 6 TOPS NPU). ARMv8.2-A.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RAM:&lt;&#x2F;strong&gt; 8 GB LPDDR4X on the module I bought. 16 GB and 32 GB variants exist.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;&#x2F;strong&gt; 64 GB eMMC on-module for OS + 4× M.2 Key-M 2280 slots, each &lt;strong&gt;PCIe Gen 3 x1&lt;&#x2F;strong&gt; (so ~1 GB&#x2F;s per slot, not x4).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Network:&lt;&#x2F;strong&gt; 1× 2.5 GbE (Realtek RTL8125B).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;USB:&lt;&#x2F;strong&gt; 2× USB 3.0 Type-A, 1× USB 3.0 Type-C (DisplayPort alt-mode), 1× USB 2.0.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Video:&lt;&#x2F;strong&gt; 2× HDMI out, 1× HDMI &lt;strong&gt;in&lt;&#x2F;strong&gt; (RK3588’s capture block).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Power:&lt;&#x2F;strong&gt; 12 V barrel jack. A 60 W brick is more than enough.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The board is small — roughly 10 × 10 cm — and passive over the SoC plus a small fan blowing across the NVMe drives. Mine sits flat on a shelf without a case.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nvme-compatibility-caveat&quot;&gt;NVMe compatibility caveat&lt;&#x2F;h3&gt;
&lt;p&gt;The one gotcha worth knowing: the CM3588 NAS Kit is &lt;strong&gt;not compatible with some WD Blue SN580 and WD Black SN850 drives&lt;&#x2F;strong&gt; — the linkup flaps at boot. I use Samsung 990 EVO and Crucial P3 Plus; both are fine. Check the FriendlyElec wiki compatibility list before buying NVMe drives.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;os-and-filesystem&quot;&gt;OS and filesystem&lt;&#x2F;h2&gt;
&lt;p&gt;I run &lt;strong&gt;Debian 13 (trixie)&lt;&#x2F;strong&gt; with the &lt;strong&gt;mainline kernel&lt;&#x2F;strong&gt; — RK3588 support is good enough from 6.8 onwards for headless use. The FriendlyElec-provided Debian image works out of the box, but I reinstall on top of it to get a clean Debian rather than their BSP.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Root filesystem:&lt;&#x2F;strong&gt; &lt;code&gt;ext4&lt;&#x2F;code&gt; on eMMC.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Data pool:&lt;&#x2F;strong&gt; &lt;code&gt;zfs&lt;&#x2F;code&gt; &lt;code&gt;raidz2&lt;&#x2F;code&gt; across the four NVMe drives. With 4× 2 TB, &lt;code&gt;raidz2&lt;&#x2F;code&gt; gives ~3.5 TB usable with two-disk redundancy. Yes, &lt;code&gt;raidz2&lt;&#x2F;code&gt; on four drives is conservative; it also means a double failure doesn’t eat the pool, and on NVMe the rebuild is fast enough that I don’t lose sleep.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ZFS module:&lt;&#x2F;strong&gt; DKMS against the running kernel. Rebuilds automatically on kernel upgrades. Has worked without manual intervention across three kernel bumps.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ARC tuning:&lt;&#x2F;strong&gt; Capped at 2 GB. This leaves headroom on an 8 GB system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Throughput is bottlenecked by 2.5 GbE for network I&#x2F;O (~280 MB&#x2F;s sustained) long before ZFS or the NVMe drives break a sweat. Local operations on the pool hit around 2 GB&#x2F;s aggregate sequential read.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;services&quot;&gt;Services&lt;&#x2F;h2&gt;
&lt;p&gt;Kept deliberately simple:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Samba&lt;&#x2F;strong&gt; for SMB shares to the Macs and Windows box on the LAN.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;rsync + cron&lt;&#x2F;strong&gt; for nightly backups from three other machines.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ZFS snapshots&lt;&#x2F;strong&gt; every hour, retained for a week. &lt;code&gt;zfs send&lt;&#x2F;code&gt; pipes the dataset weekly to an off-site B2 bucket via &lt;code&gt;zfs-autobackup&lt;&#x2F;code&gt; + rclone.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Tailscale&lt;&#x2F;strong&gt; for off-LAN access. No port-forwarding, no dyndns, no VPN concentrator. SSH and Samba both ride the Tailscale interface when I’m away.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cockpit&lt;&#x2F;strong&gt; for the occasional “why is the fan loud” dashboard moment.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;That’s it. No Docker swarm, no Kubernetes, no dozen sidecar containers. The board does one job — serve files on the LAN and let me pull backups off-site — and it does it quietly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;power-and-noise&quot;&gt;Power and noise&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idle&lt;&#x2F;strong&gt; (all four NVMe spun up, no traffic): &lt;strong&gt;~10 W&lt;&#x2F;strong&gt; at the wall.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Busy&lt;&#x2F;strong&gt; (NVMe read saturating 2.5 GbE, ZFS ARC warm): &lt;strong&gt;~18–20 W&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Peak&lt;&#x2F;strong&gt; (scrub across the whole pool, all cores hot): &lt;strong&gt;~25 W&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Compared to the 70–80 W tower it replaced, the ROI on electricity alone is about &lt;strong&gt;500 kWh&#x2F;year&lt;&#x2F;strong&gt;, or roughly &lt;strong&gt;1,500 kr&#x2F;year&lt;&#x2F;strong&gt; at Danish residential rates. The board paid for itself in about a year.&lt;&#x2F;p&gt;
&lt;p&gt;Noise: effectively inaudible from more than a metre away. The fan is small and 80% of the time it’s barely spinning.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-didn-t-work-or-took-effort&quot;&gt;What didn’t work, or took effort&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vendor kernel.&lt;&#x2F;strong&gt; The FriendlyElec-shipped kernel is FriendlyElec’s fork of 6.1 with RK-specific patches. It works for their turnkey NAS image, but DKMS ZFS builds are painful against it. Moving to mainline fixed this.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;HDMI capture.&lt;&#x2F;strong&gt; The RK3588 has a hardware HDMI input block. In mainline it’s still experimental. I have a rough use case for it (pulling a signal from an old device for archival) but haven’t made it work end-to-end. Parked for now.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;NPU.&lt;&#x2F;strong&gt; The 6 TOPS NPU is underused. Mainline has basic RKNPU support but for anything practical you still need FriendlyElec’s &lt;code&gt;rknn-toolkit2&lt;&#x2F;code&gt;. Not a blocker for a NAS.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;why-not-x86&quot;&gt;Why not x86?&lt;&#x2F;h3&gt;
&lt;p&gt;An x86 mini-PC with 4× NVMe is ~2× the idle power and ~2× the purchase price (once you include the case, PSU, and CPU). For a home NAS where the workload is “serve files” rather than “run 40 containers”, ARM wins.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-not-raspberry-pi-5&quot;&gt;Why not Raspberry Pi 5?&lt;&#x2F;h3&gt;
&lt;p&gt;The Pi 5 has one PCIe 2.0 x1 lane exposed via an HAT — you can bolt on an NVMe, but only one, and at ~500 MB&#x2F;s ceiling. The CM3588 has four lanes at PCIe 3.0 wired directly. Different class of board.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-not-openmediavault-or-truenas&quot;&gt;Why not OpenMediaVault or TrueNAS?&lt;&#x2F;h3&gt;
&lt;p&gt;Both would work. I prefer plain Debian + Samba + ZFS because the upgrade path for Debian is well-known to me, and OMV&#x2F;TrueNAS are a layer I’d have to re-learn. Your mileage will vary.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-raidz2-on-four-drives-and-not-mirror-mirror&quot;&gt;Why &lt;code&gt;raidz2&lt;&#x2F;code&gt; on four drives and not &lt;code&gt;mirror + mirror&lt;&#x2F;code&gt;?&lt;&#x2F;h3&gt;
&lt;p&gt;I considered it. &lt;code&gt;mirror + mirror&lt;&#x2F;code&gt; gives you faster writes and easier expansion but only 4 TB usable from 4× 2 TB, same as &lt;code&gt;raidz2&lt;&#x2F;code&gt;. With &lt;code&gt;raidz2&lt;&#x2F;code&gt; any two drives can fail; with striped mirrors you can lose two drives, but only if they’re in different mirror pairs. On four drives the capacity is identical and &lt;code&gt;raidz2&lt;&#x2F;code&gt; has the stronger failure tolerance.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-it-run-plex-jellyfin&quot;&gt;Can it run Plex &#x2F; Jellyfin?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, and the RK3588’s hardware H.265&#x2F;VP9&#x2F;AV1 decode block does the heavy lifting. Jellyfin with &lt;code&gt;rkmpp&lt;&#x2F;code&gt; accelerated decoding handles 4K HEVC transcodes at about 3× realtime. I don’t run either on this box — my Plex lives elsewhere — but it’s viable.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-it-suspend&quot;&gt;Does it suspend?&lt;&#x2F;h3&gt;
&lt;p&gt;Not reliably. Leave it on; idle is 10 W anyway.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-about-noise-and-heat-in-summer&quot;&gt;What about noise and heat in summer?&lt;&#x2F;h3&gt;
&lt;p&gt;Passive heatsink plus one small fan. In a Danish summer (20–25 °C ambient) it tops out around 55 °C die temp under a ZFS scrub. No throttling I’ve measured.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;&#x2F;h2&gt;
&lt;p&gt;If you want a &lt;strong&gt;quiet, low-power, all-NVMe home NAS with enough PCIe to matter&lt;&#x2F;strong&gt; and you’re willing to spend an evening on Debian + ZFS, the CM3588 NAS Kit is the best option I’ve found. It’s not the cheapest — a second-hand Optiplex is cheaper upfront — but on three-year TCO including electricity it pays back clearly.&lt;&#x2F;p&gt;
&lt;p&gt;The RK3588 itself is a surprisingly capable SoC for homelab workloads. Once mainline kernel support landed, I stopped treating it as “an ARM curiosity” and started treating it as a default answer for small always-on servers.&lt;&#x2F;p&gt;
&lt;p&gt;Two years in, mine has been rebooted twice (once for a kernel upgrade, once because I moved the shelf). I’d buy it again.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>fcrackzip — recover lost zip passwords on Linux, macOS, and Windows (2026)</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/crack-password-encrypted-zip/"/>
        <id>https://www.x2q.net/post/crack-password-encrypted-zip/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/crack-password-encrypted-zip/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;fcrackzip&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is a free, open-source command-line tool for recovering the password of an encrypted &lt;code&gt;.zip&lt;&#x2F;code&gt; file. It supports &lt;strong&gt;brute-force&lt;&#x2F;strong&gt; (over a user-defined charset and length) and &lt;strong&gt;dictionary&lt;&#x2F;strong&gt; attacks, works on Linux&#x2F;macOS&#x2F;WSL, and is the right first tool when you need to recover a forgotten password on a zip you legitimately own. It is only fast against &lt;strong&gt;classic ZipCrypto&lt;&#x2F;strong&gt; encryption; for the newer &lt;strong&gt;AES-256&lt;&#x2F;strong&gt; zip profile, &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; will not work — you want &lt;code&gt;zip2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt; instead. Install with &lt;code&gt;apt install fcrackzip&lt;&#x2F;code&gt; (Debian&#x2F;Ubuntu), &lt;code&gt;brew install fcrackzip&lt;&#x2F;code&gt; (macOS), or build from source.&lt;&#x2F;p&gt;
&lt;p&gt;This post walks through the practical cases. It assumes you have &lt;strong&gt;authorisation to recover the password&lt;&#x2F;strong&gt; — your own files, a client’s, a CTF. Don’t use it against zips you don’t own.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# Debian &#x2F; Ubuntu &#x2F; Mint &#x2F; Kali
sudo apt install fcrackzip

# macOS (Homebrew)
brew install fcrackzip

# Arch &#x2F; Manjaro
sudo pacman -S fcrackzip

# Windows
# Use WSL2 + Ubuntu and `apt install fcrackzip`.
# A native .exe exists but is old — WSL is simpler.

# From source
git clone https:&#x2F;&#x2F;github.com&#x2F;hyc&#x2F;fcrackzip.git
cd fcrackzip &amp;amp;&amp;amp; .&#x2F;configure &amp;amp;&amp;amp; make
sudo cp fcrackzip &#x2F;usr&#x2F;local&#x2F;bin&#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;check-what-kind-of-encryption-your-zip-uses-first&quot;&gt;Check what kind of encryption your zip uses first&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;fcrackzip&lt;&#x2F;code&gt; only handles &lt;strong&gt;classic ZipCrypto&lt;&#x2F;strong&gt; (the original, weak PKZIP encryption). For modern &lt;strong&gt;AES-128 &#x2F; AES-256&lt;&#x2F;strong&gt; zips (introduced by WinZip 9 in 2003, now default in some tools), &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; will silently fail to find the password no matter how long you run it.&lt;&#x2F;p&gt;
&lt;p&gt;Quickest check with &lt;code&gt;zipinfo&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ zipinfo -v archive.zip | head -40
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Look for &lt;code&gt;WinZip AES encryption, strength 3&lt;&#x2F;code&gt; (= AES-256). If you see that, skip to the hashcat section.&lt;&#x2F;p&gt;
&lt;p&gt;Or with &lt;code&gt;7z&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ 7z l -slt archive.zip | grep -i &amp;#39;method\|encrypted&amp;#39;
Method = ZipCrypto Store          ← fcrackzip works
Method = AES-256 Deflate          ← fcrackzip will NOT work
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;basic-brute-force&quot;&gt;Basic brute-force&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;fcrackzip -v -b -c &amp;#39;a1&amp;#39; -p aaaa -u archive.zip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-v&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — verbose.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-b&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — brute-force mode.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-c &#x27;a1&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — charset. &lt;code&gt;a&lt;&#x2F;code&gt; = lowercase &lt;code&gt;a–z&lt;&#x2F;code&gt;, &lt;code&gt;A&lt;&#x2F;code&gt; = uppercase, &lt;code&gt;1&lt;&#x2F;code&gt; = digits, &lt;code&gt;!&lt;&#x2F;code&gt; = printable symbols. Combine: &lt;code&gt;&#x27;aA1!&#x27;&lt;&#x2F;code&gt; = all printable ASCII (94 chars).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-p aaaa&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — starting password (defines minimum length = 4 here).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-u&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — only report if the decrypted file “unzips” cleanly (filters false positives — ZipCrypto has a small CRC that many random passwords pass).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Length: &lt;code&gt;-p aaaaa&lt;&#x2F;code&gt; starts at 5 characters. &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; doesn’t have a &lt;code&gt;--max-length&lt;&#x2F;code&gt; flag; it increments the starting password through the charset forever, so &lt;strong&gt;letting it run to “completion” at length 8 on the full printable ASCII charset is about 94⁸ ≈ 6×10¹⁵ candidates&lt;&#x2F;strong&gt; — not realistic. You bound the attack by picking a reasonable max charset + length, or by using a dictionary.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dictionary-attack&quot;&gt;Dictionary attack&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;fcrackzip -v -D -u -p &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt archive.zip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-D&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — dictionary mode.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-p &amp;lt;file&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — wordlist file, one candidate per line.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-u&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — verify via unzip, as before.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; is the standard 14 M-entry wordlist shipped with Kali and available everywhere. If you have context about the password (it’s someone’s name + year, it’s a project codename), assemble a smaller targeted wordlist — hit rates are usually much higher than any generic list.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;speed-and-what-to-expect&quot;&gt;Speed and what to expect&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;fcrackzip&lt;&#x2F;code&gt; is single-threaded and CPU-bound. On a modern laptop you’ll see roughly:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;10–30 million ZipCrypto attempts per second per core&lt;&#x2F;strong&gt; for a dictionary attack against a typical zip.&lt;&#x2F;li&gt;
&lt;li&gt;Brute-force slightly faster because less string I&#x2F;O.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;That makes it fine for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dictionary attacks&lt;&#x2F;strong&gt; against any reasonable wordlist.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Short brute-force&lt;&#x2F;strong&gt; (4–6 chars alphanumeric).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It makes it not fine for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Length 8+ with symbols&lt;&#x2F;strong&gt; — you’ll never finish.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;AES-256 zips&lt;&#x2F;strong&gt; — wrong tool.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;when-to-reach-for-hashcat-instead&quot;&gt;When to reach for hashcat instead&lt;&#x2F;h2&gt;
&lt;p&gt;If &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; is too slow, or the zip is AES-encrypted, migrate to &lt;code&gt;hashcat&lt;&#x2F;code&gt;. It’s the general-purpose password-cracking tool, GPU-accelerated, and handles both ZipCrypto and AES WinZip hashes.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;step-1-extract-the-hash-with-zip2john&quot;&gt;Step 1: extract the hash with &lt;code&gt;zip2john&lt;&#x2F;code&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;zip2john&lt;&#x2F;code&gt; is part of John the Ripper. Ubuntu: &lt;code&gt;apt install john&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ zip2john archive.zip &amp;gt; hash.txt
$ cat hash.txt
archive.zip:$zip2$*0*3*0*...*$&#x2F;zip2$::archive.zip:secret.pdf:&#x2F;path&#x2F;to&#x2F;archive.zip
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;step-2-feed-it-to-hashcat&quot;&gt;Step 2: feed it to hashcat&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;# ZipCrypto (mode 17200, 17210, 17220, 17225 depending on compression)
hashcat -m 17200 hash.txt &#x2F;path&#x2F;to&#x2F;wordlist.txt

# AES-encrypted WinZip (mode 13600)
hashcat -m 13600 hash.txt &#x2F;path&#x2F;to&#x2F;wordlist.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On a single modern discrete GPU (e.g. RTX 4090) you’ll see:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ZipCrypto: ~5–10 billion H&#x2F;s&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WinZip AES-256: ~10–50 million H&#x2F;s&lt;&#x2F;strong&gt; (much slower because AES is expensive and PBKDF2 iterations are involved)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;hashcat’s rule engine, mask attacks (&lt;code&gt;-a 3&lt;&#x2F;code&gt;), and combinator mode (&lt;code&gt;-a 1&lt;&#x2F;code&gt;) make it vastly more flexible than &lt;code&gt;fcrackzip&lt;&#x2F;code&gt;. For anything non-trivial it is the right tool.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;common-gotchas&quot;&gt;Common gotchas&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;password-found-pw-abc-but-unzip-still-asks-for-a-password&quot;&gt;“PASSWORD FOUND!!!!: pw == abc” but &lt;code&gt;unzip&lt;&#x2F;code&gt; still asks for a password&lt;&#x2F;h3&gt;
&lt;p&gt;You didn’t use &lt;code&gt;-u&lt;&#x2F;code&gt;. fcrackzip’s CRC check has false positives. Re-run with &lt;code&gt;-u&lt;&#x2F;code&gt; to verify candidates against a real decompression attempt.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;different-files-in-the-same-zip-have-different-passwords&quot;&gt;Different files in the same zip have different passwords&lt;&#x2F;h3&gt;
&lt;p&gt;Classic PKZIP supports per-file passwords. &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; attacks one file at a time; pass &lt;code&gt;-l &amp;lt;filename&amp;gt;&lt;&#x2F;code&gt; (or let it pick the first encrypted one) if you need to be specific.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fcrackzip-finds-the-password-but-i-still-can-t-unzip&quot;&gt;fcrackzip finds the password, but I still can’t unzip&lt;&#x2F;h3&gt;
&lt;p&gt;Character set &#x2F; locale issue. If the password contains non-ASCII characters, the zip’s creator may have used a different encoding than your terminal. Try &lt;code&gt;unzip -P &quot;$(echo -n &#x27;passwd&#x27; | iconv -t cp437)&quot; archive.zip&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;aes-encrypted-and-dictionary-doesn-t-find-it&quot;&gt;AES-encrypted and dictionary doesn’t find it&lt;&#x2F;h3&gt;
&lt;p&gt;hashcat + rules + a targeted wordlist is the move. See the migration steps above.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;defensive-takeaway&quot;&gt;Defensive takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;If you’re generating zip files you want to stay encrypted: use &lt;strong&gt;AES-256&lt;&#x2F;strong&gt;, not ZipCrypto. Most modern tools (&lt;code&gt;7z&lt;&#x2F;code&gt;, &lt;code&gt;zip&lt;&#x2F;code&gt; from InfoZIP with &lt;code&gt;-e&lt;&#x2F;code&gt;, WinZip, Keka) give you AES if you ask. The default on &lt;code&gt;zip&lt;&#x2F;code&gt; unfortunately is still ZipCrypto on many systems. Check with &lt;code&gt;7z l -slt archive.zip&lt;&#x2F;code&gt; before shipping it.&lt;&#x2F;p&gt;
&lt;p&gt;And pick a password with length, not cleverness. &lt;code&gt;hashcat&lt;&#x2F;code&gt; doesn’t care how weird your substitution pattern is; it only cares about entropy. A four-word passphrase (&lt;code&gt;correct-horse-battery-staple&lt;&#x2F;code&gt;-style, ~44 bits) is fine against offline ZipCrypto attacks; a random 12-character mixed password (~78 bits) is fine against GPU AES attacks. Anything shorter is recoverable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-fcrackzip-legal&quot;&gt;Is fcrackzip legal?&lt;&#x2F;h3&gt;
&lt;p&gt;Recovering passwords on files you own or are authorised to access is legal in every jurisdiction I’m aware of. Doing it on someone else’s file without authorisation is unauthorised access — the same law that applies to any other computer intrusion.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-fcrackzip-maintained&quot;&gt;Is fcrackzip maintained?&lt;&#x2F;h3&gt;
&lt;p&gt;Barely. The codebase has been stable for fifteen-plus years. For modern work, the combination of &lt;code&gt;zip2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt; has eclipsed it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-fcrackzip-use-a-gpu&quot;&gt;Can fcrackzip use a GPU?&lt;&#x2F;h3&gt;
&lt;p&gt;No. &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; is CPU-only. If GPU is what you have, go straight to hashcat.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-fcrackzip-handle-7z-rar-gpg-files&quot;&gt;Does fcrackzip handle .7z, .rar, .gpg files?&lt;&#x2F;h3&gt;
&lt;p&gt;No — only classic PKZIP &lt;code&gt;.zip&lt;&#x2F;code&gt;. For &lt;code&gt;.7z&lt;&#x2F;code&gt;, use &lt;code&gt;7z2john&lt;&#x2F;code&gt; + hashcat (mode 11600). For &lt;code&gt;.rar&lt;&#x2F;code&gt;, &lt;code&gt;rar2john&lt;&#x2F;code&gt; + hashcat (mode 12500 &#x2F; 13000). For &lt;code&gt;.gpg&lt;&#x2F;code&gt;, &lt;code&gt;gpg2john&lt;&#x2F;code&gt; + hashcat (mode 17010+).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-generate-a-targeted-wordlist&quot;&gt;How do I generate a targeted wordlist?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;hashcat&lt;&#x2F;code&gt;’s maskprocessor (&lt;code&gt;mp64&lt;&#x2F;code&gt;), &lt;code&gt;crunch&lt;&#x2F;code&gt;, and &lt;code&gt;cewl&lt;&#x2F;code&gt; (crawls a site for candidate words) are all worth knowing. For recovering a specific forgotten password, writing down what you remember about it — length, likely words, likely numbers — and building a mask attack from that is the highest-yield approach.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fcrackzip&lt;&#x2F;code&gt; + &lt;code&gt;-D&lt;&#x2F;code&gt; + a wordlist is the right first try.&lt;&#x2F;li&gt;
&lt;li&gt;If that doesn’t work and it’s ZipCrypto: &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; + &lt;code&gt;-b&lt;&#x2F;code&gt; with a narrow charset and short length.&lt;&#x2F;li&gt;
&lt;li&gt;If that doesn’t work or it’s AES: &lt;code&gt;zip2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt; on a GPU.&lt;&#x2F;li&gt;
&lt;li&gt;If that doesn’t work: your password was strong enough. That’s the system working as intended.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>pdfcrack — recover lost PDF passwords on Linux and macOS (2026)</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/crack-password-protected-pdf-pdfcrack/"/>
        <id>https://www.x2q.net/post/crack-password-protected-pdf-pdfcrack/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/crack-password-protected-pdf-pdfcrack/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;strong&gt;&lt;code&gt;pdfcrack&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is a command-line tool that recovers the &lt;strong&gt;user password&lt;&#x2F;strong&gt; or &lt;strong&gt;owner password&lt;&#x2F;strong&gt; of an encrypted PDF by brute-force or dictionary attack. It’s small, written in C, runs on Linux&#x2F;macOS&#x2F;WSL, and handles every encryption profile PDF has shipped — 40-bit RC4, 128-bit RC4, 128-bit AES, 256-bit AES (PDF 1.7 + 2.0). It is &lt;strong&gt;single-threaded and CPU-only&lt;&#x2F;strong&gt;, so it is fast on the weak old RC4-40 encryption but increasingly slow as PDFs get modern. Install with &lt;code&gt;apt install pdfcrack&lt;&#x2F;code&gt; (Debian&#x2F;Ubuntu), &lt;code&gt;brew install pdfcrack&lt;&#x2F;code&gt; (macOS), or build from source. When &lt;code&gt;pdfcrack&lt;&#x2F;code&gt; is too slow, switch to &lt;strong&gt;&lt;code&gt;pdf2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; on a GPU.&lt;&#x2F;p&gt;
&lt;p&gt;This walkthrough assumes you have &lt;strong&gt;authorisation to recover the password&lt;&#x2F;strong&gt; on the PDF — your own document, a client’s, or a legally-acquired file. Don’t attack PDFs you don’t own.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;user-password-vs-owner-password&quot;&gt;User password vs owner password&lt;&#x2F;h2&gt;
&lt;p&gt;PDF has two passwords:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User password&lt;&#x2F;strong&gt; — required to &lt;strong&gt;open and read&lt;&#x2F;strong&gt; the document.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Owner password&lt;&#x2F;strong&gt; — required to &lt;strong&gt;remove restrictions&lt;&#x2F;strong&gt; (print, copy, modify) on a document that opens without a user password.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Many “protected” PDFs are only owner-password-restricted: they open fine, but printing and copying are disabled by the reader. Those are trivial to unlock — &lt;code&gt;qpdf --decrypt&lt;&#x2F;code&gt; or any of the &lt;code&gt;pdftk&lt;&#x2F;code&gt;-style tools strip owner-only restrictions without needing to guess anything. &lt;code&gt;pdfcrack&lt;&#x2F;code&gt; is for the harder case: a &lt;strong&gt;user-password-protected PDF you can’t open at all&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Check which you’re dealing with:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ qpdf --show-encryption document.pdf
R = 6
P = -3904
User password = 
Supplied password is owner password
extract for accessibility: allowed
extract for any purpose: not allowed
print low resolution: allowed
print high resolution: not allowed
modify document assembly: not allowed
modify forms: not allowed
modify annotations: not allowed
modify other: not allowed
stream encryption method: AESv3
string encryption method: AESv3
file encryption method: AESv3
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;R = ...&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; tells you the revision &#x2F; profile. &lt;code&gt;R=2&lt;&#x2F;code&gt; = RC4-40, &lt;code&gt;R=3&lt;&#x2F;code&gt; = RC4-128, &lt;code&gt;R=4&lt;&#x2F;code&gt; = AES-128, &lt;code&gt;R=5&#x2F;6&lt;&#x2F;code&gt; = AES-256.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;“Supplied password is owner password”&lt;&#x2F;strong&gt; + “User password = (empty)” means you can open it and just need to strip restrictions. Use &lt;code&gt;qpdf --decrypt --password=&#x27;&#x27; input.pdf output.pdf&lt;&#x2F;code&gt; and you’re done.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;# Debian &#x2F; Ubuntu &#x2F; Mint &#x2F; Kali
sudo apt install pdfcrack

# macOS (Homebrew)
brew install pdfcrack

# Arch &#x2F; Manjaro
sudo pacman -S pdfcrack

# Fedora &#x2F; RHEL
sudo dnf install pdfcrack

# From source
wget https:&#x2F;&#x2F;sourceforge.net&#x2F;projects&#x2F;pdfcrack&#x2F;files&#x2F;latest&#x2F;download -O pdfcrack.tar.gz
tar xf pdfcrack.tar.gz &amp;amp;&amp;amp; cd pdfcrack-*
make
sudo cp pdfcrack &#x2F;usr&#x2F;local&#x2F;bin&#x2F;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;benchmark&quot;&gt;Benchmark&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;$ pdfcrack -b
Benchmark:	Average Speed (calls &#x2F; second):
MD5:			1728972.6
MD5_50 (fast):		 97879.3
MD5_50 (slow):		 69167.0

RC4 (40, static):	606555.3
RC4 (40, no check):	598050.0
RC4 (128, no check):	590141.7

Benchmark:	Average Speed (passwords &#x2F; second):
PDF (40, user):		453510.2
PDF (40, owner):	220250.0
PDF (40, owner, fast):	499995.0

PDF (128, user):	 22000.0
PDF (128, owner):	 10408.7
PDF (128, owner, fast):	 22220.0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Translation:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RC4-40&lt;&#x2F;strong&gt; (PDF 1.3, very old) — ~450k passwords&#x2F;sec. Brute-force of a full 8-character alphanumeric space: ~1 week on one core.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RC4-128 &#x2F; AES-128&lt;&#x2F;strong&gt; — ~22k passwords&#x2F;sec. Brute-force becomes unrealistic above length 6 with any charset.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;AES-256 (R=5&#x2F;6)&lt;&#x2F;strong&gt; — even slower; &lt;code&gt;pdfcrack&lt;&#x2F;code&gt; does support it but effectively only for dictionary attacks against small wordlists.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you need speed beyond this, you want &lt;code&gt;hashcat&lt;&#x2F;code&gt; on a GPU. Numbers at the bottom.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dictionary-attack&quot;&gt;Dictionary attack&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;pdfcrack -f document.pdf -w &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-f&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — target file.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-w &amp;lt;file&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — wordlist, one candidate per line.&lt;&#x2F;li&gt;
&lt;li&gt;Optional &lt;strong&gt;&lt;code&gt;-o&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — attack the owner password only (ignore user password). Default is to try user password first.&lt;&#x2F;li&gt;
&lt;li&gt;Optional &lt;strong&gt;&lt;code&gt;-u&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — attack user password only.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you have context — the password is probably a first name, a year, a project code — assemble a smaller targeted list. Generic lists are a long shot once you’re past the top ten thousand entries.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;brute-force-attack&quot;&gt;Brute-force attack&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;pdfcrack -f document.pdf -c &amp;#39;abcdefghijklmnopqrstuvwxyz0123456789&amp;#39; -n 4 -m 8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-c &amp;lt;charset&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — character set to try. Default includes a broad ASCII range.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-n &amp;lt;min&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — minimum length.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-m &amp;lt;max&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — maximum length.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Tune &lt;code&gt;-c&lt;&#x2F;code&gt; aggressively. If you know the password is all lowercase, don’t give it uppercase. If you know it has a digit at the end, use a mask attack (hashcat territory, not pdfcrack).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;resume-a-long-run&quot;&gt;Resume a long run&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;pdfcrack -f document.pdf -w rockyou.txt -s &#x2F;tmp&#x2F;state.sav
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-s&lt;&#x2F;code&gt; writes periodic save files; if you kill pdfcrack and restart, pass &lt;code&gt;-l &#x2F;tmp&#x2F;state.sav&lt;&#x2F;code&gt; to resume.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-to-switch-to-hashcat&quot;&gt;When to switch to hashcat&lt;&#x2F;h2&gt;
&lt;p&gt;For any modern PDF (R=4, 5, or 6 — i.e. AES-128 or AES-256), &lt;code&gt;pdfcrack&lt;&#x2F;code&gt;’s single-core speed becomes the bottleneck. &lt;code&gt;pdf2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt; gets you two to three orders of magnitude more throughput on a GPU.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;step-1-extract-the-hash&quot;&gt;Step 1: extract the hash&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;$ &#x2F;usr&#x2F;share&#x2F;john&#x2F;pdf2john.pl document.pdf &amp;gt; hash.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(On Debian&#x2F;Ubuntu: &lt;code&gt;apt install john&lt;&#x2F;code&gt; provides &lt;code&gt;pdf2john.pl&lt;&#x2F;code&gt;.)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;step-2-identify-the-hash-mode&quot;&gt;Step 2: identify the hash mode&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;$ head -1 hash.txt
document.pdf:$pdf$5*6*256*-1028*1*16*...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Map the first two numbers:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$pdf$1*2*...&lt;&#x2F;code&gt; → &lt;strong&gt;mode 10400&lt;&#x2F;strong&gt; (PDF 1.1–1.3, RC4-40, user)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$pdf$1*2*...&lt;&#x2F;code&gt; with owner salt → &lt;strong&gt;mode 10410 &#x2F; 10420&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$pdf$2*3*...&lt;&#x2F;code&gt; → &lt;strong&gt;mode 10500&lt;&#x2F;strong&gt; (PDF 1.4–1.6, RC4-128 &#x2F; AES-128)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$pdf$5*5*...&lt;&#x2F;code&gt; → &lt;strong&gt;mode 10600&lt;&#x2F;strong&gt; (PDF 1.7 r=5, AES-256)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$pdf$5*6*...&lt;&#x2F;code&gt; → &lt;strong&gt;mode 10700&lt;&#x2F;strong&gt; (PDF 1.7 r=6 &#x2F; PDF 2.0, AES-256 with PBKDF2)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;step-3-run-hashcat&quot;&gt;Step 3: run hashcat&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code&gt;hashcat -m 10700 hash.txt &#x2F;path&#x2F;to&#x2F;wordlist.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On a single modern GPU (e.g. RTX 4090):&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mode 10400 (RC4-40):&lt;&#x2F;strong&gt; ~10 billion H&#x2F;s — any practical password is recovered instantly.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mode 10500 (RC4-128 &#x2F; AES-128):&lt;&#x2F;strong&gt; ~100 million H&#x2F;s.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mode 10700 (AES-256, PBKDF2):&lt;&#x2F;strong&gt; ~50,000 H&#x2F;s. This is by design — the PBKDF2 iterations make brute-force expensive.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For mode 10700, your attack strategy matters more than raw speed. Dictionary + rules + mask attacks targeted at likely password patterns are orders of magnitude more productive than exhaustive brute-force.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;common-gotchas&quot;&gt;Common gotchas&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;file-seems-encrypted-but-i-can-still-open-it-without-a-password&quot;&gt;“File seems encrypted but I can still open it without a password”&lt;&#x2F;h3&gt;
&lt;p&gt;You’re looking at owner-only restrictions. Use &lt;code&gt;qpdf --decrypt input.pdf output.pdf&lt;&#x2F;code&gt; — no cracking needed.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pdfcrack-runs-but-never-finds-anything&quot;&gt;pdfcrack runs but never finds anything&lt;&#x2F;h3&gt;
&lt;p&gt;Three likely causes:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Password is longer &#x2F; more complex than your charset × length covers.&lt;&#x2F;strong&gt; Increase &lt;code&gt;-m&lt;&#x2F;code&gt; and&#x2F;or expand &lt;code&gt;-c&lt;&#x2F;code&gt;, or switch to a dictionary.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Password contains non-ASCII characters.&lt;&#x2F;strong&gt; pdfcrack’s charset is ASCII by default; non-ASCII user passwords in old PDFs used varying encodings (PDFDocEncoding, UTF-16). Try &lt;code&gt;pdf2john&lt;&#x2F;code&gt; + hashcat, which handles the encoding properly.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;It’s AES-256 (R=6) and you’re being patient.&lt;&#x2F;strong&gt; See speed notes above — realistic only with a dictionary.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;only-aes-256-is-supported-error-or-similar&quot;&gt;“Only AES-256 is supported” error or similar&lt;&#x2F;h3&gt;
&lt;p&gt;Your &lt;code&gt;pdfcrack&lt;&#x2F;code&gt; build is old. Ubuntu LTS sometimes ships a version that doesn’t fully handle R=6. Build from source or switch to &lt;code&gt;pdf2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pdf-opens-but-printing-copying-is-blocked&quot;&gt;PDF opens but printing&#x2F;copying is blocked&lt;&#x2F;h3&gt;
&lt;p&gt;That’s owner-password-restricted only. &lt;code&gt;qpdf --decrypt&lt;&#x2F;code&gt; without needing a password usually works:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;qpdf --decrypt --password=&amp;#39;&amp;#39; input.pdf output.pdf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;defensive-takeaway&quot;&gt;Defensive takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;For anything you want to stay encrypted in 2026, use &lt;strong&gt;AES-256 with a PDF 2.0 (R=6) password&lt;&#x2F;strong&gt;. That’s the profile with PBKDF2 key derivation that makes offline attacks expensive on current hardware.&lt;&#x2F;p&gt;
&lt;p&gt;And, as always, the password’s length matters more than anything else. A 20-character random passphrase is brute-force-intractable against any current GPU. A 6-character “clever substitution” password is a coffee break on an RTX 4090.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-pdfcrack-still-maintained&quot;&gt;Is pdfcrack still maintained?&lt;&#x2F;h3&gt;
&lt;p&gt;Updated slowly. The codebase handles all currently shipped PDF encryption profiles; it just isn’t where performance work is happening anymore (that’s hashcat).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-pdfcrack-use-gpu&quot;&gt;Does pdfcrack use GPU?&lt;&#x2F;h3&gt;
&lt;p&gt;No. CPU-only, single-threaded. For GPU work, use &lt;code&gt;pdf2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-pdfcrack-recover-the-document-if-the-creator-cleared-the-user-password-but-kept-the-owner-password&quot;&gt;Can pdfcrack recover the document if the creator cleared the user password but kept the owner password?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — use &lt;code&gt;qpdf --decrypt&lt;&#x2F;code&gt; first; if that won’t fully decrypt, &lt;code&gt;pdfcrack -o&lt;&#x2F;code&gt; attacks the owner password.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-pdfcrack-leak-my-pdf-anywhere&quot;&gt;Does pdfcrack leak my PDF anywhere?&lt;&#x2F;h3&gt;
&lt;p&gt;No. It’s local. Anything online-only (“crack my PDF at cloud-service-X.com”) involves uploading the file, which you shouldn’t do for confidential material.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-if-i-only-vaguely-remember-the-password&quot;&gt;What if I only vaguely remember the password?&lt;&#x2F;h3&gt;
&lt;p&gt;Write down what you remember — likely words, likely digits, likely length — and build a targeted wordlist or &lt;code&gt;hashcat&lt;&#x2F;code&gt; mask (&lt;code&gt;-a 3 ?l?l?l?l?l?d?d?d?d&lt;&#x2F;code&gt;). That beats any generic list.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-recover-text-from-an-aes-256-pdf-without-the-password&quot;&gt;Can I recover text from an AES-256 PDF without the password?&lt;&#x2F;h3&gt;
&lt;p&gt;No. AES-256 with PBKDF2 (R=6) is, to the best of public cryptanalysis, not broken. If the password is strong and lost, the contents are lost.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Owner-password-only?&lt;&#x2F;strong&gt; &lt;code&gt;qpdf --decrypt&lt;&#x2F;code&gt;, no cracking needed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;User-password-protected, old (R=2&#x2F;3)?&lt;&#x2F;strong&gt; &lt;code&gt;pdfcrack&lt;&#x2F;code&gt; with a dictionary works fine.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Modern (R=4&#x2F;5&#x2F;6)?&lt;&#x2F;strong&gt; &lt;code&gt;pdf2john&lt;&#x2F;code&gt; + &lt;code&gt;hashcat&lt;&#x2F;code&gt; on a GPU, targeted attack.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Strong 20-character random?&lt;&#x2F;strong&gt; Accept the loss.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hacking Wi-Fi across 25 years — 2002, 2007, 2012, 2017, 2022, 2027</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/hack-wireless-network/"/>
        <id>https://www.x2q.net/post/hack-wireless-network/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/hack-wireless-network/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; Wi-Fi security has gone through four generations — &lt;strong&gt;WEP&lt;&#x2F;strong&gt;, &lt;strong&gt;WPA&lt;&#x2F;strong&gt;, &lt;strong&gt;WPA2&lt;&#x2F;strong&gt;, &lt;strong&gt;WPA3&lt;&#x2F;strong&gt; — each introduced because its predecessor was broken. This post walks through six snapshots of the state of the art: &lt;strong&gt;2002&lt;&#x2F;strong&gt; (WEP, trivial), &lt;strong&gt;2007&lt;&#x2F;strong&gt; (WEP dead, WPA widespread, offline attacks emerging), &lt;strong&gt;2012&lt;&#x2F;strong&gt; (WPA2 dominant, dictionary attacks on GPUs), &lt;strong&gt;2017&lt;&#x2F;strong&gt; (KRACK breaks WPA2 under the right conditions, PMKID leak in 2018), &lt;strong&gt;2022&lt;&#x2F;strong&gt; (WPA3 rolling out, downgrade attacks, cloud-scale cracking), &lt;strong&gt;2027&lt;&#x2F;strong&gt; (today’s landscape: WPA3 nearly universal, 6 GHz opens new surface, attacks move up the stack). For each year I summarise the attack, the tool, and what a defender should have been doing. Authorised testing only — the defensive guidance at the bottom is the point.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;at-a-glance&quot;&gt;At a glance&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Year&lt;&#x2F;th&gt;&lt;th&gt;Dominant standard&lt;&#x2F;th&gt;&lt;th&gt;Headline attack&lt;&#x2F;th&gt;&lt;th&gt;Effort to compromise a typical network&lt;&#x2F;th&gt;&lt;th&gt;Fastest dictionary &#x2F; key-rate at the time&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2002&lt;&#x2F;td&gt;&lt;td&gt;WEP (100%)&lt;&#x2F;td&gt;&lt;td&gt;FMS statistical IV&lt;&#x2F;td&gt;&lt;td&gt;~4M packets &#x2F; 2–10 h on a busy AP&lt;&#x2F;td&gt;&lt;td&gt;N&#x2F;A — key recovered from IVs&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2007&lt;&#x2F;td&gt;&lt;td&gt;WEP 35%, WPA 25%, WPA2 20%, open 20%&lt;&#x2F;td&gt;&lt;td&gt;PTW + aircrack-ng&lt;&#x2F;td&gt;&lt;td&gt;40–85k packets &#x2F; &amp;lt;5 min (WEP)&lt;&#x2F;td&gt;&lt;td&gt;~50–100 PSK&#x2F;s (CPU, cowpatty)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2012&lt;&#x2F;td&gt;&lt;td&gt;WPA2 ~70%&lt;&#x2F;td&gt;&lt;td&gt;Reaver (WPS) + GPU PSK dictionary&lt;&#x2F;td&gt;&lt;td&gt;11,000 WPS PINs &#x2F; 2–10 h&lt;&#x2F;td&gt;&lt;td&gt;~75,000 PSK&#x2F;s (GTX 580)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2017&lt;&#x2F;td&gt;&lt;td&gt;WPA2 ~85%&lt;&#x2F;td&gt;&lt;td&gt;KRACK; then PMKID (2018)&lt;&#x2F;td&gt;&lt;td&gt;1 packet (PMKID) → offline dict&lt;&#x2F;td&gt;&lt;td&gt;~400,000 PSK&#x2F;s (GTX 1080)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2022&lt;&#x2F;td&gt;&lt;td&gt;WPA2 ~60%, WPA3 ~20%, mixed ~15%&lt;&#x2F;td&gt;&lt;td&gt;Dragonblood downgrade&lt;&#x2F;td&gt;&lt;td&gt;Force WPA2 fallback, then GPU&#x2F;cloud&lt;&#x2F;td&gt;&lt;td&gt;~1.2M PSK&#x2F;s (RTX 3090), ~10M (8× A100)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2027&lt;&#x2F;td&gt;&lt;td&gt;WPA3 ~40%, mixed ~35%, WPA2 ~20%&lt;&#x2F;td&gt;&lt;td&gt;Client-side + downgrade + chipset FW&lt;&#x2F;td&gt;&lt;td&gt;SAE intact; legacy WPA2 on borrowed time&lt;&#x2F;td&gt;&lt;td&gt;~5–8M PSK&#x2F;s (RTX 5090-era single GPU)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Approximate global deployment shares are from public Wi-Fi surveys (WiGLE aggregate trends + vendor reports); treat as ± 10 points.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2002-wep-everywhere-and-trivially-broken&quot;&gt;2002 — WEP everywhere, and trivially broken&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; IEEE 802.11b with &lt;strong&gt;WEP&lt;&#x2F;strong&gt; (Wired Equivalent Privacy). 40-bit or 104-bit RC4 key, 24-bit IV, CRC-32 integrity.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What’s wrong with it:&lt;&#x2F;strong&gt; The IV is too small, keys are static, and RC4’s initial key schedule leaks bits of the key when certain IVs are used. These are the &lt;strong&gt;FMS attack&lt;&#x2F;strong&gt; (Fluhrer, Mantin, Shamir, 2001) and its descendants.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What an attacker did in 2002:&lt;&#x2F;strong&gt; captured traffic with a Prism-based USB adapter, collected a few million packets (hours on an active network), and extracted the key with &lt;strong&gt;AirSnort&lt;&#x2F;strong&gt; or &lt;strong&gt;WEPCrack&lt;&#x2F;strong&gt;. Attacks were so embarrassing that Wi-Fi Alliance pushed WPA as a temporary firmware-patchable fix in 2003.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2002):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global Wi-Fi deployment:&lt;&#x2F;strong&gt; ~100% WEP (WPA not ratified until March 2003).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Key length:&lt;&#x2F;strong&gt; 40-bit (export-grade) or 104-bit. Irrelevant — the attack is on the IV, not the key.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IV space:&lt;&#x2F;strong&gt; 24 bits → only ~16.7 M possible IVs. Birthday-paradox collision around ~5,000 packets.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;FMS attack packet count:&lt;&#x2F;strong&gt; ~500 k–4 M “interesting” IV frames (a few hours of active traffic).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Typical crack wall-clock:&lt;&#x2F;strong&gt; 2–10 h on a busy SOHO network.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Consumer hardware required:&lt;&#x2F;strong&gt; one laptop + one Prism2&#x2F;Prism2.5 USB dongle (~$40 used at the time).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Tools:&lt;&#x2F;strong&gt; AirSnort, WEPCrack, Kismet for capture.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CVEs:&lt;&#x2F;strong&gt; WEP itself isn’t a CVE, but the 2001 Borisov&#x2F;Goldberg&#x2F;Wagner paper and the 2001 FMS paper are the canonical breakage references.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What a defender should have done:&lt;&#x2F;strong&gt; realise WEP is not a security feature and treat the wireless LAN as untrusted. Use a VPN or IPSec at layer 3.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2007-wep-dead-wpa-transitional-wpa2-arriving&quot;&gt;2007 — WEP dead, WPA transitional, WPA2 arriving&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; WPA (2003), a stopgap with per-packet keys (TKIP). WPA2 (2004) with AES-CCMP starting to appear in home routers.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What’s new for attackers:&lt;&#x2F;strong&gt; the &lt;strong&gt;PTW attack&lt;&#x2F;strong&gt; on WEP (2007) drops the packet count needed from ~1 million to ~40,000 — WEP now cracks in minutes. &lt;strong&gt;aircrack-ng&lt;&#x2F;strong&gt; becomes the canonical toolkit. Against WPA-Personal, offline dictionary attacks on the 4-way-handshake become practical on CPUs: capture a handshake, feed the PSK derivation through &lt;code&gt;cowpatty&lt;&#x2F;code&gt; or &lt;code&gt;aircrack-ng&lt;&#x2F;code&gt;, try dictionary entries one at a time.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What attackers didn’t have yet:&lt;&#x2F;strong&gt; meaningful GPU acceleration of WPA-PSK. Dictionary attacks were slow.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2007):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global deployment (WiGLE-style surveys):&lt;&#x2F;strong&gt; WEP ~35%, WPA ~25%, WPA2 ~20%, open ~20%.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PTW attack packet count:&lt;&#x2F;strong&gt; ~40,000–85,000 captured frames to recover a WEP key (down from millions).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Crack time with aircrack-ng + packet injection:&lt;&#x2F;strong&gt; &amp;lt;5 minutes, often under 60 seconds.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WPA-PSK dictionary rate (CPU, cowpatty&#x2F;aircrack-ng):&lt;&#x2F;strong&gt; ~50–100 candidates&#x2F;sec per core.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Rockyou wordlist&lt;&#x2F;strong&gt; (leaked Dec 2009): 14.3 M entries — became the de-facto English PSK wordlist.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WPA2 mandatory for Wi-Fi CERTIFIED:&lt;&#x2F;strong&gt; March 2006.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Aircrack-ng 1.0:&lt;&#x2F;strong&gt; released 2006, consolidating the ecosystem.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What a defender should have done:&lt;&#x2F;strong&gt; migrate to WPA2-AES-CCMP, pick a PSK longer than any dictionary. Drop WEP networks entirely.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2012-wpa2-dominant-gpu-cracking-mature&quot;&gt;2012 — WPA2 dominant, GPU-cracking mature&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; WPA2 is the default on essentially all consumer gear. WPA2-Enterprise (802.1X &#x2F; EAP) in corporate networks.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What’s new for attackers:&lt;&#x2F;strong&gt; &lt;strong&gt;hashcat&lt;&#x2F;strong&gt; and &lt;strong&gt;oclHashcat&lt;&#x2F;strong&gt;, &lt;strong&gt;pyrit&lt;&#x2F;strong&gt;, and &lt;strong&gt;John the Ripper&lt;&#x2F;strong&gt; turn the WPA-PSK dictionary attack into a &lt;strong&gt;GPU problem&lt;&#x2F;strong&gt;. A handshake capture + a single modern GPU does tens of thousands of candidate PSKs per second; a small rented GPU farm handles hundreds of thousands. &lt;strong&gt;airodump-ng&lt;&#x2F;strong&gt; + &lt;strong&gt;Reaver&lt;&#x2F;strong&gt; (2011) exploit a &lt;strong&gt;WPS PIN&lt;&#x2F;strong&gt; implementation flaw to recover the WPA-PSK without a dictionary at all — a dealbreaker for home routers with WPS-on-by-default.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2012):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global deployment:&lt;&#x2F;strong&gt; WPA2 ~70%, WEP ~10%, open ~15%, WPA ~5%.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WPS PIN space on paper:&lt;&#x2F;strong&gt; 10⁸ = 100 million. &lt;strong&gt;Effective&lt;&#x2F;strong&gt; attack space after the Viehböck split: 10⁴ + 10³ = ~11,000 tries.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reaver wall-clock to recover WPS PIN → PSK:&lt;&#x2F;strong&gt; 2–10 hours against an unpatched AP.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hashcat WPA-PSK throughput on GTX 580 (reference 2012 GPU):&lt;&#x2F;strong&gt; ~75,000 candidates&#x2F;sec.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hashcat WPA-PSK on a small 4-GPU rig of the era:&lt;&#x2F;strong&gt; ~300,000 candidates&#x2F;sec, i.e. &lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; (14.3 M) in ~50 seconds.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Real-world PSK recovery rate with rockyou + common rules:&lt;&#x2F;strong&gt; ~20–25% on captured handshakes in academic studies.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;WPA2-PEAP&#x2F;MSCHAPv2 hash rate (a Wi-Fi-adjacent Enterprise pain point):&lt;&#x2F;strong&gt; tens of millions&#x2F;sec on a single GPU, full 2²⁸ keyspace within a day.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What defenders should have done:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turn off WPS.&lt;&#x2F;strong&gt; Most routers had it on by default.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Use long random PSKs&lt;&#x2F;strong&gt;, not dictionary words.&lt;&#x2F;li&gt;
&lt;li&gt;For enterprise: &lt;strong&gt;802.1X + EAP-TLS with client certificates&lt;&#x2F;strong&gt;, not PEAP-MSCHAPv2 (which is itself a GPU problem).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;2017-krack&quot;&gt;2017 — KRACK&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; WPA2 still everywhere.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What’s new for attackers:&lt;&#x2F;strong&gt; &lt;strong&gt;KRACK&lt;&#x2F;strong&gt; (Vanhoef, Piessens, 2017) — Key Reinstallation Attack. It doesn’t crack the PSK. It exploits a subtle flaw in the 4-way handshake where, under certain conditions (specifically on Android 6+ and wpa_supplicant 2.4–2.6), replaying handshake message 3 causes the client to reinstall an &lt;strong&gt;all-zero session key&lt;&#x2F;strong&gt;. From there, the attacker can decrypt selected traffic and, depending on cipher suite, inject packets.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What made KRACK different from prior attacks:&lt;&#x2F;strong&gt; it hit the &lt;strong&gt;protocol, not the key&lt;&#x2F;strong&gt;. A strong PSK didn’t help. Every WPA2 implementation needed a patch. Most vendors shipped one within weeks; some took years.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Also in this era:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PMKID hashcat attack&lt;&#x2F;strong&gt; (2018, Steube). Many APs leak the PMKID in a single message; one packet is enough to start an offline PSK attack. Lowered the capture bar further — no client association needed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Evil twin &#x2F; captive portal phishing.&lt;&#x2F;strong&gt; As device UIs got prettier, fake captive portals became alarmingly effective against humans.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2017–2018):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global deployment:&lt;&#x2F;strong&gt; WPA2 ~85%, WEP &amp;lt;3%, open ~10%, WPA3 not yet shipping.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;KRACK CVEs:&lt;&#x2F;strong&gt; 10 in the coordinated disclosure (CVE-2017-13077 through CVE-2017-13088).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;KRACK-affected devices:&lt;&#x2F;strong&gt; all WPA2 clients and APs — &lt;strong&gt;10+ billion&lt;&#x2F;strong&gt; endpoints. Real patching took years for long-tail IoT.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Vendor patch latency:&lt;&#x2F;strong&gt; Microsoft &#x2F; most Linux distros within 1 week of disclosure; Android 2 months+ depending on OEM; many embedded devices never patched.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PMKID attack packet count:&lt;&#x2F;strong&gt; &lt;strong&gt;1 single frame&lt;&#x2F;strong&gt; vs. the 4-way handshake’s 4 frames — no client association or deauth needed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hashcat on GTX 1080 (reference 2017 GPU):&lt;&#x2F;strong&gt; ~400,000 WPA-PSK candidates&#x2F;sec — ~5× faster than 2012.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A rented 8× 1080 Ti instance (~$4&#x2F;h in 2017):&lt;&#x2F;strong&gt; ~3M candidates&#x2F;sec — &lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; in &amp;lt;5 seconds.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Coordinated disclosure lead time:&lt;&#x2F;strong&gt; 4 months from vendor notification to public release.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What a defender should have done:&lt;&#x2F;strong&gt; patch Android &#x2F; wpa_supplicant immediately, enforce HTTPS everywhere so decrypted Wi-Fi traffic is less useful, start planning WPA3 migration.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2022-wpa3-rolling-out-downgrade-attacks&quot;&gt;2022 — WPA3 rolling out, downgrade attacks&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; WPA3 (2018) required for Wi-Fi CERTIFIED devices since 2020. Key upgrades:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SAE&lt;&#x2F;strong&gt; (Simultaneous Authentication of Equals) replaces the 4-way handshake PSK — offline dictionary attacks become infeasible.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;192-bit mode&lt;&#x2F;strong&gt; for Enterprise.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Forward secrecy&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PMF&lt;&#x2F;strong&gt; (Protected Management Frames) mandatory — neutralises deauth-based denial-of-service.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What’s new for attackers:&lt;&#x2F;strong&gt; WPA3 isn’t broken, but its deployment mostly isn’t WPA3-&lt;strong&gt;only&lt;&#x2F;strong&gt;. &lt;strong&gt;WPA2&#x2F;3 transition mode&lt;&#x2F;strong&gt; — the default on most APs — means an attacker can force a downgrade and attack as WPA2. This is &lt;strong&gt;Dragonblood&lt;&#x2F;strong&gt; (Vanhoef, Ronen, 2019 + follow-ups): a family of side-channel and downgrade attacks against SAE. By 2022, patched SAE implementations mostly closed the timing side-channels; the downgrade attack still works on mixed-mode networks.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Also in this era:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud-scale cracking&lt;&#x2F;strong&gt;: renting 8× H100 or similar on an hourly basis makes PMK dictionary attacks practical at a scale no amateur had in 2012.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Chipset firmware vulnerabilities&lt;&#x2F;strong&gt; (FragAttacks, 2021): flaws in the Wi-Fi stack itself, below the WPA layer, let attackers inject frames without needing any handshake break at all.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2022):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global deployment:&lt;&#x2F;strong&gt; WPA2-only ~60%, WPA3 ~20%, WPA2&#x2F;3 transition mode ~15%, WEP &amp;lt;1%.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;New-router WPA3 support:&lt;&#x2F;strong&gt; ~60% of consumer APs shipped in 2022 supported WPA3 (vs. ~10% in 2020).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Dragonblood CVEs:&lt;&#x2F;strong&gt; 5 (CVE-2019-9494 through CVE-2019-9499).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;FragAttacks CVEs:&lt;&#x2F;strong&gt; 12 in the 2021 bundle.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hashcat WPA-PSK on RTX 3090:&lt;&#x2F;strong&gt; ~1.2M candidates&#x2F;sec.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;8× A100 cloud instance:&lt;&#x2F;strong&gt; ~10M candidates&#x2F;sec aggregate. &lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; in ~1.5 seconds; a 10B-candidate mask attack in ~17 minutes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cost per billion PSK attempts on spot cloud GPUs:&lt;&#x2F;strong&gt; ~$3–5.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;12-character fully-random PSK entropy:&lt;&#x2F;strong&gt; 2⁷² ≈ 4.7 × 10²¹. At 10M H&#x2F;s, exhaustive attack = &lt;strong&gt;~15,000 years&lt;&#x2F;strong&gt;. Safe.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;8-character lowercase+digit PSK entropy:&lt;&#x2F;strong&gt; 2⁴¹ ≈ 2.2 × 10¹². At 10M H&#x2F;s, &lt;strong&gt;~2.5 days&lt;&#x2F;strong&gt;. Not safe.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What a defender should have done:&lt;&#x2F;strong&gt; WPA3-Personal only (no WPA2 transition) on networks where you can enforce it; PMF required; long random PSK even with SAE; keep AP firmware current.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2027-today-s-landscape&quot;&gt;2027 — today’s landscape&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Standard:&lt;&#x2F;strong&gt; Wi-Fi 6E (6 GHz) and early Wi-Fi 7 deployments. WPA3 near-universal on new gear; legacy WPA2 still common in small businesses and long-lived home networks.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;State of the art in attacks:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SAE is holding up.&lt;&#x2F;strong&gt; No public, practical attack against a well-configured WPA3-Personal network in 2027. Dictionary attacks against the PSK are not feasible — that’s SAE’s point.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Downgrade attacks&lt;&#x2F;strong&gt; are the main thing. Any network still advertising WPA2&#x2F;3 transition is attackable as WPA2. Adversaries look for mixed-mode SSIDs first.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;6 GHz opens new surface.&lt;&#x2F;strong&gt; The 6 GHz band requires WPA3 for new standards; but legacy client bridging, IoT devices without 6 GHz support, and misconfigured co-broadcast SSIDs create inconsistencies attackers exploit.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Client-side attacks.&lt;&#x2F;strong&gt; When the link layer is secure, attackers move up. Rogue captive portals that trick phones into trusting a bad certificate, QR-code joining with fake SSIDs, deauth-during-roaming attacks (harder now with PMF but not eliminated).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Supply chain.&lt;&#x2F;strong&gt; Firmware-embedded backdoors and vendor bugs in Wi-Fi chipsets (the FragAttacks legacy) remain a durable source of exploits.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cloud cracking is commoditised.&lt;&#x2F;strong&gt; A weekend-long SAE-offline attack (for the cases where offline attack is possible, i.e. the target ran an unpatched SAE) costs roughly the price of a nice dinner. This mostly doesn’t matter because SAE isn’t reducible to offline dictionary attack — but it shifts economics on legacy WPA2 networks that people haven’t migrated.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Tools still in rotation in 2027:&lt;&#x2F;strong&gt; aircrack-ng (older, still canonical for captures and WPA2), hashcat (cracking), bettercap (Wi-Fi MITM &#x2F; rogue AP), airgeddon (workflow glue), and a wave of ESP32-based pocket tools for opportunistic deauth and handshake capture.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;By the numbers (2027):&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global deployment:&lt;&#x2F;strong&gt; WPA3 ~40%, WPA2&#x2F;3 transition ~35%, WPA2-only ~20%, WEP ~0.5% (long tail of ancient gear).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise share running WPA3-Enterprise 192-bit mode:&lt;&#x2F;strong&gt; still &amp;lt;10%. Most corporate networks remain WPA2-Enterprise with PEAP.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;New-device WPA3 certification:&lt;&#x2F;strong&gt; mandatory for Wi-Fi 6 since 2020, Wi-Fi 6E (6 GHz) since 2022, Wi-Fi 7 at launch.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hashcat WPA-PSK on RTX 5090-class single GPU (2025+):&lt;&#x2F;strong&gt; ~5–8M candidates&#x2F;sec.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;8× H100 cloud instance:&lt;&#x2F;strong&gt; ~50M candidates&#x2F;sec aggregate. &lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; in ~0.3 seconds.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SAE (WPA3-Personal) offline attack:&lt;&#x2F;strong&gt; infeasible at any hash rate — the handshake does not release a crackable hash to an eavesdropper.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cost to brute-force a weak 8-char WPA2 PSK in 2027:&lt;&#x2F;strong&gt; &amp;lt;$10 on spot cloud GPUs.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cost to brute-force a 12-char random WPA2 PSK:&lt;&#x2F;strong&gt; still ~ $10⁹ (same math as 2022, GPUs 5× faster → still millennia).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ESP32-S3 “Wi-Fi Nugget” &#x2F; Flipper Zero-class pocket tools&lt;&#x2F;strong&gt; in circulation in 2027: hundreds of thousands of units. Opportunistic handshake capture is trivial; PSK cracking is still offline on a beefy host.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deauth-DoS effectiveness:&lt;&#x2F;strong&gt; near-zero on PMF-required networks; still works against the ~30% of deployed APs where PMF is “capable” but not “required”.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Bottom line: against &lt;strong&gt;WPA3-Personal with SAE, PMF required, and a random PSK&lt;&#x2F;strong&gt;, none of the 2027 toolkit gets you in via the link layer. Against &lt;strong&gt;a typical 2019-vintage home router still running WPA2 with a weak PSK and WPS optionally exposed&lt;&#x2F;strong&gt;, a weekend on a mid-range GPU is plenty.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;defensive-guidance-for-2027&quot;&gt;Defensive guidance for 2027&lt;&#x2F;h2&gt;
&lt;p&gt;This is the part that matters more than any of the history.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WPA3-Personal SAE-only, or WPA3-Enterprise with EAP-TLS.&lt;&#x2F;strong&gt; No transition mode on any network you can control. Check explicitly — many APs default to transition.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PMF required, not optional.&lt;&#x2F;strong&gt; Protected Management Frames prevent deauth-driven handshake captures and basic denial-of-service.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PSK length: random, ≥ 20 characters.&lt;&#x2F;strong&gt; SAE makes short PSKs safer than they used to be, but no reason to gamble.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Disable WPS permanently.&lt;&#x2F;strong&gt; Even though the WPS-PIN attack is ancient, many routers still ship with it enabled for “convenience”.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Guest SSID isolated.&lt;&#x2F;strong&gt; Client-to-client isolation on and routed to a separate VLAN with no internal LAN access.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IoT SSID, separate and cloistered.&lt;&#x2F;strong&gt; Cheap Wi-Fi thermostats and toys are the weakest link on most home networks. They get their own SSID, their own VLAN, and no route to anything important.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Firmware updates on APs.&lt;&#x2F;strong&gt; Most of the interesting 2020s Wi-Fi vulnerabilities were chipset-level; the fix is always a firmware update. Buy APs from a vendor that ships them.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Assume the link layer will fail and defend above it.&lt;&#x2F;strong&gt; HTTPS everywhere, client certs on anything that matters, VPN or Tailscale for remote access. When (not if) someone breaks your Wi-Fi, layer 4+ should still hold.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-stayed-the-same-across-25-years&quot;&gt;What stayed the same across 25 years&lt;&#x2F;h2&gt;
&lt;p&gt;Three patterns repeat at every generation:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The cryptography is almost always stronger than the deployments.&lt;&#x2F;strong&gt; WEP was a design failure; WPA2 and WPA3 have been broken by implementation flaws, downgrade paths, and side channels — not by pure crypto weaknesses.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Defaults matter more than capabilities.&lt;&#x2F;strong&gt; WPS on by default, WPA2&#x2F;3 transition mode on by default, guest networks without isolation by default — this is what attackers actually find, at scale.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The attacker always moves up the stack.&lt;&#x2F;strong&gt; When link-layer security improves, attacks shift to captive portals, rogue certs, client mis-configurations, and phishing. No amount of WPA3 fixes the user who clicks “Trust” on the evil twin’s fake certificate.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;coda&quot;&gt;Coda&lt;&#x2F;h2&gt;
&lt;p&gt;If this post’s ancestor on this site, from 2010, was “How to Hack a Wireless Network” and focused on WEP with &lt;code&gt;aircrack-ng&lt;&#x2F;code&gt;, the 2027 version is closer to “How to Think About Wi-Fi Security” and focuses on configuration, patching, and realistic threat modelling. The cryptography has done its job. The layers around it are the interesting frontier now.&lt;&#x2F;p&gt;
&lt;p&gt;Test your own networks — or networks you’re authorised to test — don’t test anyone else’s. The same law that applied in 2002 applies in 2027.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>nemboks.dk — forward your Danish digital post (mit.dk, e-Boks) to your email inbox</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/nemboks-dk-digital-post-forwarding/"/>
        <id>https://www.x2q.net/post/nemboks-dk-digital-post-forwarding/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/nemboks-dk-digital-post-forwarding/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;nemboks.dk&quot;&gt;nemboks.dk&lt;&#x2F;a&gt; is a service that &lt;strong&gt;forwards Danish digital post&lt;&#x2F;strong&gt; (from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mit.dk&quot;&gt;mit.dk&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.e-boks.com&#x2F;&quot;&gt;e-Boks&lt;&#x2F;a&gt;) straight to your email. The problem it solves: digital post is “push” in theory but pull in practice — you still have to log in with MitID, open an app, and click around to read a letter from SKAT or your kommune. Nemboks turns that into an email that lands in the inbox you already live in, attachments and all. Built for Danish SMBs where the same &lt;code&gt;virksomhed&lt;&#x2F;code&gt; receives post for multiple companies or employees, and where accountants, bookkeepers, and property managers want digital post in the same place as every other email. Free beta; after beta, 39 kr&#x2F;md on annual billing, 49 kr&#x2F;md monthly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem-with-danish-digital-post&quot;&gt;The problem with Danish digital post&lt;&#x2F;h2&gt;
&lt;p&gt;Since 2014, communication from the Danish public sector is digital by default. That means if your kommune, SKAT, Udbetaling Danmark, or FerieKonto needs to reach you, they drop a letter into either &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mit.dk&quot;&gt;mit.dk&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; (the public portal) or &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.e-boks.com&#x2F;&quot;&gt;e-Boks&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; (historically the most used private portal, now share-gated with Digital Post).&lt;&#x2F;p&gt;
&lt;p&gt;In principle this is great: centralised, auditable, no paper. In practice it’s friction-heavy:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You need MitID to log in every time.&lt;&#x2F;strong&gt; Every tax letter, parking ticket, or vacation-pay notice is gated behind an MFA flow.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The apps notify you, but the notifications are thin.&lt;&#x2F;strong&gt; “You have new post” — not a subject line, not a sender, not enough to triage.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;There’s no native forwarding.&lt;&#x2F;strong&gt; You can’t say “send everything from SKAT to my accountant at &lt;code&gt;bogholder@firma.dk&lt;&#x2F;code&gt;.”&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Businesses inherit the worst of both worlds.&lt;&#x2F;strong&gt; A CVR-registered company has its own mit.dk &#x2F; e-Boks inbox. If the owner doesn’t check it, nobody does.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Email, by contrast, is the default inbox for small businesses: shared mailboxes, forwarding rules, archiving, filters, search. Nemboks bridges the two.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-nemboks-does&quot;&gt;What Nemboks does&lt;&#x2F;h2&gt;
&lt;p&gt;Once set up, Nemboks authenticates against your mit.dk &#x2F; e-Boks on your behalf, polls your inbox for new post, and forwards each new letter — &lt;strong&gt;as a real email with the PDF attached&lt;&#x2F;strong&gt; — to the address(es) you configure.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One email per letter.&lt;&#x2F;strong&gt; Subject = sender + subject. Body = plain-text preview. Attachment = the PDF.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Multiple companies per account.&lt;&#x2F;strong&gt; If you’re a revisor managing post for many CVRs, they all live under one Nemboks dashboard.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Multiple destinations per company.&lt;&#x2F;strong&gt; Forward everything from SKAT to the bogholder, everything else to the owner, for example.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Keeps the original in mit.dk &#x2F; e-Boks.&lt;&#x2F;strong&gt; Nothing is deleted — Nemboks only reads.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Logs every forward.&lt;&#x2F;strong&gt; For audit and peace of mind.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The practical outcome: the accountant or bookkeeper who already lives in Outlook or Gmail stops context-switching into a portal once a week.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;who-it-s-for&quot;&gt;Who it’s for&lt;&#x2F;h2&gt;
&lt;p&gt;Nemboks is built for small- and medium-sized Danish businesses, especially:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accountants and bookkeepers (&lt;code&gt;revisorer&lt;&#x2F;code&gt;, &lt;code&gt;bogholdere&lt;&#x2F;code&gt;).&lt;&#x2F;strong&gt; Clients give them Nemboks access once; from then on, every SKAT letter for every client turns into an email in the shared inbox.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Property managers (&lt;code&gt;ejendomsadministratorer&lt;&#x2F;code&gt;).&lt;&#x2F;strong&gt; A single administrator might manage post for dozens of property-owning A&#x2F;S and ApS.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Small-company owners.&lt;&#x2F;strong&gt; The &lt;code&gt;enkeltmandsvirksomhed&lt;&#x2F;code&gt; or two-person &lt;code&gt;ApS&lt;&#x2F;code&gt; whose owner would rather receive a letter from their municipality in the same place as their customer email.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-stack&quot;&gt;The stack&lt;&#x2F;h2&gt;
&lt;p&gt;For the curious:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rails 8&lt;&#x2F;strong&gt; on Ruby 3.4, using Hotwire (Turbo + Stimulus) and Tailwind for the dashboard.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;&#x2F;strong&gt; for persistence.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;auth0.com&#x2F;&quot;&gt;Auth0&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; for authentication. Users sign in with Google, Microsoft, or email+password — no separate Nemboks password to forget.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stripe.com&#x2F;&quot;&gt;Stripe&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; for subscription management, including the hosted customer portal for self-service card updates and cancellations.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postmarkapp.com&#x2F;&quot;&gt;Postmark&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; for transactional email — it’s the category leader for “email that must land in the inbox,” which is the entire point of a forwarding service.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Docker + Kamal&lt;&#x2F;strong&gt; for deployment. Zero-downtime rollouts on a small fleet; no Kubernetes to operate.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Pages&lt;&#x2F;strong&gt; for the static marketing site at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;nemboks.dk&quot;&gt;nemboks.dk&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The split — Rails app on Kamal, marketing site on Pages — is deliberate. The marketing site needs to be cheap, fast, and heavily cached; the Rails app needs a database and background jobs. Running them as two separate deployments keeps each one simple.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pricing&quot;&gt;Pricing&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free beta&lt;&#x2F;strong&gt; while the service is stabilising.&lt;&#x2F;li&gt;
&lt;li&gt;After beta: &lt;strong&gt;39 kr&#x2F;md&lt;&#x2F;strong&gt; on annual billing, &lt;strong&gt;49 kr&#x2F;md&lt;&#x2F;strong&gt; monthly — VAT not included. Flat per-company pricing; no per-letter fees.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;60-day free trial&lt;&#x2F;strong&gt; for new customers after beta ends.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-nemboks-allowed-to-read-my-mit-dk-e-boks&quot;&gt;Is Nemboks allowed to read my mit.dk &#x2F; e-Boks?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — as the inbox owner, you authorise Nemboks to read on your behalf. Nemboks only reads; it does not delete, reply, or mark as read anything you didn’t configure.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-this-gdpr-compliant&quot;&gt;Is this GDPR-compliant?&lt;&#x2F;h3&gt;
&lt;p&gt;Nemboks processes personal data on your behalf. As a customer you’re the data controller; Nemboks is the data processor, and there’s a standard databehandleraftale (DPA) to sign. All data is stored in the EU.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-happens-to-the-original-letter-in-mit-dk-e-boks&quot;&gt;What happens to the original letter in mit.dk &#x2F; e-Boks?&lt;&#x2F;h3&gt;
&lt;p&gt;It stays there. Nemboks is read-only.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-forward-to-multiple-email-addresses&quot;&gt;Can I forward to multiple email addresses?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes. Each company can have multiple forwarding destinations, and you can route different senders to different addresses (for example, everything from SKAT to the bookkeeper).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-nemboks-work-for-private-individuals&quot;&gt;Does Nemboks work for private individuals?&lt;&#x2F;h3&gt;
&lt;p&gt;The current focus is Danish businesses (CVR). A private-individual plan may come later.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-if-nemboks-goes-down&quot;&gt;What if Nemboks goes down?&lt;&#x2F;h3&gt;
&lt;p&gt;Your post still arrives in mit.dk &#x2F; e-Boks normally; Nemboks just won’t forward it until the service is back. Nothing is lost.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-cancel&quot;&gt;How do I cancel?&lt;&#x2F;h3&gt;
&lt;p&gt;Through the Stripe customer portal from inside the dashboard. No email, no phone call.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-build-it&quot;&gt;Why build it&lt;&#x2F;h2&gt;
&lt;p&gt;Digital post works. It’s secure, it’s auditable, and I’d rather trust a SKAT letter in mit.dk than a brown envelope. But the user interface is designed for a citizen reading a handful of letters a year, not for a business receiving dozens a week across several CVRs. Email solves the “dozens a week across several mailboxes” problem well — filters, rules, shared inboxes, search — so the smallest useful bridge is to get the letters out of the portal and into email, with the PDF attached, without anyone having to log in every time.&lt;&#x2F;p&gt;
&lt;p&gt;That’s Nemboks.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How to play Spotify on a Logitech Squeezebox in 2026</title>
        <published>2026-04-22T00:00:00+00:00</published>
        <updated>2026-04-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/squeezebox-spotify-guide/"/>
        <id>https://www.x2q.net/post/squeezebox-spotify-guide/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/squeezebox-spotify-guide/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; The official Spotify plugin Logitech shipped with Squeezebox stopped working when Spotify retired the &lt;code&gt;libspotify&lt;&#x2F;code&gt; API in 2022. In 2026 there are two working paths to get &lt;strong&gt;Spotify on a Squeezebox Classic, Touch, Boom, Radio, or Transporter&lt;&#x2F;strong&gt;: (1) &lt;strong&gt;Logitech Media Server + the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;michaelherger&#x2F;Spotty-Plugin&quot;&gt;Spotty plugin&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; (now renamed &lt;code&gt;Spotty&lt;&#x2F;code&gt; &#x2F; sometimes &lt;code&gt;Spotty-XL&lt;&#x2F;code&gt;), which uses the Spotify Web API plus &lt;code&gt;librespot&lt;&#x2F;code&gt; under the hood, or (2) run &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Spotifyd&#x2F;spotifyd&quot;&gt;spotifyd&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; on the same host as LMS and point LMS at it as a generic audio source. Path 1 is the one you want unless you have a reason otherwise. This post walks through it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-happened-to-the-official-plugin&quot;&gt;What happened to the official plugin&lt;&#x2F;h2&gt;
&lt;p&gt;Squeezebox hardware went end-of-life at Logitech in 2012, but the software — &lt;strong&gt;Logitech Media Server (LMS)&lt;&#x2F;strong&gt;, the thing that actually streams music to the devices — has been community-maintained ever since. For years it shipped an official “Spotify” plugin that used Spotify’s now-discontinued &lt;code&gt;libspotify&lt;&#x2F;code&gt; C library. When Spotify shut &lt;code&gt;libspotify&lt;&#x2F;code&gt; down in 2022, that plugin stopped working, and Spotify has not shipped a replacement for third-party embedded devices.&lt;&#x2F;p&gt;
&lt;p&gt;The community filled the gap:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;librespot&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — an open-source, Rust reimplementation of the Spotify Connect client protocol. It’s the basis of most modern third-party Spotify integrations.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Spotty plugin for LMS&lt;&#x2F;strong&gt; — wraps &lt;code&gt;librespot&lt;&#x2F;code&gt; plus the Spotify Web API into an LMS plugin. Presents Spotify browsing, search, playlists, and playback inside the Squeezebox UI (both the device screen and the LMS web UI).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;spotifyd&lt;&#x2F;strong&gt; — a standalone &lt;code&gt;librespot&lt;&#x2F;code&gt;-based daemon that shows up as a Spotify Connect target. Your phone sees it as a speaker, you cast to it, and its audio output goes somewhere LMS can pick it up.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;path-1-recommended-spotty-plugin-on-lms&quot;&gt;Path 1 (recommended): Spotty plugin on LMS&lt;&#x2F;h2&gt;
&lt;p&gt;This is the cleanest experience and the one that feels native on the Squeezebox. You search and browse from the Squeezebox remote or Touch screen, and it Just Works.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;1-get-lms-running&quot;&gt;1. Get LMS running&lt;&#x2F;h3&gt;
&lt;p&gt;If you’re not already running LMS, install it on whatever always-on machine you have — a Raspberry Pi, a NAS with a Docker runtime, or a small Linux box is ideal.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Debian&#x2F;Ubuntu — grab the latest .deb from the community repo
wget https:&#x2F;&#x2F;downloads.slimdevices.com&#x2F;LogitechMediaServer_v8.x&#x2F;logitechmediaserver_&amp;lt;latest&amp;gt;_amd64.deb
sudo apt install .&#x2F;logitechmediaserver_&amp;lt;latest&amp;gt;_amd64.deb
sudo systemctl enable --now logitechmediaserver
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or via Docker — &lt;code&gt;lmscommunity&#x2F;logitechmediaserver&lt;&#x2F;code&gt; is a maintained image:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name lms \
  --network host \
  -v lms-config:&#x2F;config \
  -v &#x2F;path&#x2F;to&#x2F;music:&#x2F;music:ro \
  lmscommunity&#x2F;logitechmediaserver:latest
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Open &lt;code&gt;http:&#x2F;&#x2F;&amp;lt;host&amp;gt;:9000&#x2F;&lt;&#x2F;code&gt; and you should see the LMS web UI. Your Squeezebox devices on the same LAN should appear under &lt;strong&gt;Players&lt;&#x2F;strong&gt; automatically.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2-install-spotty&quot;&gt;2. Install Spotty&lt;&#x2F;h3&gt;
&lt;p&gt;In the LMS web UI: &lt;strong&gt;Settings → Plugins → Install new plugins&lt;&#x2F;strong&gt; and tick &lt;strong&gt;Spotty&lt;&#x2F;strong&gt; (author: Michael Herger). LMS downloads, installs, and restarts. On restart, you’ll have a &lt;strong&gt;Spotty&lt;&#x2F;strong&gt; entry in &lt;strong&gt;Settings → Advanced → Spotty&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;3-authorise-spotty-with-your-spotify-account&quot;&gt;3. Authorise Spotty with your Spotify account&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Settings → Advanced → Spotty&lt;&#x2F;strong&gt;, click &lt;strong&gt;Add account&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Spotty kicks off an OAuth flow via Spotify’s Web API — you log in once in a browser, approve the access, and the token is stored on the LMS server.&lt;&#x2F;li&gt;
&lt;li&gt;You need &lt;strong&gt;Spotify Premium&lt;&#x2F;strong&gt; for playback. (This is a Spotify-side limit: &lt;code&gt;librespot&lt;&#x2F;code&gt; can authenticate with Free, but full-quality playback is Premium-only.)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;4-play-something&quot;&gt;4. Play something&lt;&#x2F;h3&gt;
&lt;p&gt;On the Squeezebox device itself, &lt;strong&gt;Home → My Music → Spotty&lt;&#x2F;strong&gt; (or &lt;strong&gt;Home → Music Library → Spotty&lt;&#x2F;strong&gt;, depending on your LMS version) — browse playlists, search, start a track. On the Squeezebox Touch the on-screen artwork works. On the Classic the text UI works. On the Radio and Boom you drive it from the web UI or the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;squeeze-commander.com&#x2F;&quot;&gt;Squeeze Commander&lt;&#x2F;a&gt; &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;play.google.com&#x2F;store&#x2F;apps&#x2F;details?id=uk.org.ngo.squeezer&quot;&gt;Squeezer&lt;&#x2F;a&gt; apps.&lt;&#x2F;p&gt;
&lt;p&gt;Spotty also adds Squeezebox devices as &lt;strong&gt;Spotify Connect targets&lt;&#x2F;strong&gt; — meaning you can cast from your phone’s Spotify app to “Living Room Squeezebox” natively, the same way you’d cast to a Sonos or a Google Home.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;path-2-spotifyd-as-a-connect-bridge&quot;&gt;Path 2: spotifyd as a Connect bridge&lt;&#x2F;h2&gt;
&lt;p&gt;Use this if Spotty won’t install (old LMS version, old Perl, weird OS) or you specifically want Spotify Connect behaviour without Spotty.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Debian&#x2F;Ubuntu
sudo apt install spotifyd
# or from source: cargo install spotifyd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Configure &lt;code&gt;&#x2F;etc&#x2F;spotifyd.conf&lt;&#x2F;code&gt; with your Spotify credentials, a backend (&lt;code&gt;alsa&lt;&#x2F;code&gt;, &lt;code&gt;pulseaudio&lt;&#x2F;code&gt;, or &lt;code&gt;pipe&lt;&#x2F;code&gt;), and a device name. Start the daemon:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now spotifyd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From your phone, cast to the &lt;code&gt;spotifyd&lt;&#x2F;code&gt;-named device. Audio comes out of the host.&lt;&#x2F;p&gt;
&lt;p&gt;To get that audio into a Squeezebox, pipe &lt;code&gt;spotifyd&lt;&#x2F;code&gt; to a FIFO and play the FIFO in LMS:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# spotifyd.conf
backend = &amp;quot;pipe&amp;quot;
device = &amp;quot;&#x2F;var&#x2F;run&#x2F;spotifyd.pipe&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then in LMS: add the FIFO as a &lt;strong&gt;File system music folder&lt;&#x2F;strong&gt; and play it as a live stream.&lt;&#x2F;p&gt;
&lt;p&gt;This is clunkier and has ~1 s of latency vs. Spotty’s tight integration. Only use it if Path 1 doesn’t work for you.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;works-on-which-squeezebox&quot;&gt;Works on which Squeezebox?&lt;&#x2F;h2&gt;
&lt;p&gt;Spotty works on every Squeezebox device that talks to LMS:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Squeezebox Classic &#x2F; Squeezebox v3&lt;&#x2F;strong&gt; (text display) — text-UI browsing and playback.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Squeezebox Touch&lt;&#x2F;strong&gt; — colour touchscreen, artwork, the whole UI.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Squeezebox Boom&lt;&#x2F;strong&gt; — text UI, same as Classic.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Squeezebox Radio&lt;&#x2F;strong&gt; — text UI, built-in speaker.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Squeezebox Duet &#x2F; Controller&lt;&#x2F;strong&gt; — the controller’s UI works; playback goes to the Receiver.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Transporter&lt;&#x2F;strong&gt; — full support.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;piCorePlayer &#x2F; SqueezeLite on Pi&lt;&#x2F;strong&gt; — yes, these are software Squeezeboxes and Spotty streams to them fine.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As long as the device can connect to LMS, Spotty can feed it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;do-i-need-spotify-premium&quot;&gt;Do I need Spotify Premium?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, for playback. &lt;code&gt;librespot&lt;&#x2F;code&gt; (and therefore Spotty) requires a Premium account to decode audio. Spotify Free accounts can authenticate but won’t play.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-spotify-connect-appear-the-squeezebox-as-a-speaker-work&quot;&gt;Does Spotify Connect “appear the Squeezebox as a speaker” work?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, with Spotty installed. Each Squeezebox player shows up in the Spotify app’s device picker as a Connect target.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-hi-res-lossless-work&quot;&gt;Does hi-res &#x2F; lossless work?&lt;&#x2F;h3&gt;
&lt;p&gt;Spotify itself doesn’t offer lossless on standard plans (Hi-Fi has been “coming soon” for years). Spotty plays whatever Spotify serves — Ogg Vorbis 320 kbps on Premium — and the Squeezebox handles it natively.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gapless-playback&quot;&gt;Gapless playback?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, Spotty supports gapless.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-this-break-if-spotify-s-web-api-changes&quot;&gt;Does this break if Spotify’s Web API changes?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, occasionally — Spotify periodically rotates OAuth scopes or deprecates endpoints, and Spotty gets a patch release. Keep the plugin updated.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-run-lms-on-the-same-box-as-my-nas&quot;&gt;Can I run LMS on the same box as my NAS?&lt;&#x2F;h3&gt;
&lt;p&gt;Absolutely. LMS is lightweight (~200 MB RAM, minimal CPU except during library scans). Running it next to Samba or on a homelab SBC is the common setup.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-this-the-same-as-picoreplayer&quot;&gt;Is this the same as “piCorePlayer”?&lt;&#x2F;h3&gt;
&lt;p&gt;piCorePlayer is a tiny Linux distribution that turns a Raspberry Pi into a Squeezebox-compatible player (via SqueezeLite). It does not replace LMS — you still need LMS as the server. piCorePlayer + LMS + Spotty is a very common 2026 stack for people whose original Squeezebox hardware finally died.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-to-do-if-your-squeezebox-won-t-connect-to-lms-at-all&quot;&gt;What to do if your Squeezebox won’t connect to LMS at all&lt;&#x2F;h2&gt;
&lt;p&gt;Two things to check first:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;mDNS &#x2F; multicast across your network.&lt;&#x2F;strong&gt; Modern Wi-Fi routers often block multicast between SSIDs or to wired hosts. Put the Squeezebox and LMS on the same subnet&#x2F;VLAN with multicast allowed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SlimProto port (3483&#x2F;udp and 3483&#x2F;tcp).&lt;&#x2F;strong&gt; That’s what the Squeezebox uses to find LMS. Not blocked by anything sensible, but worth checking if you run a strict firewall.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Beyond that, the Squeezebox community is still active — &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;forums.slimdevices.com&#x2F;&quot;&gt;forums.slimdevices.com&lt;&#x2F;a&gt; and the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;LMS-Community&quot;&gt;LMS Community GitHub&lt;&#x2F;a&gt; are where problems get solved.&lt;&#x2F;p&gt;
&lt;p&gt;Squeezebox hardware was discontinued fifteen years ago. Thanks to LMS, Spotty, and &lt;code&gt;librespot&lt;&#x2F;code&gt;, it still plays Spotify better than most “smart speakers” sold new in 2026.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>elpriser.org — Danish hourly electricity prices</title>
        <published>2026-04-20T00:00:00+00:00</published>
        <updated>2026-04-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/elpriser-org-danish-electricity-prices/"/>
        <id>https://www.x2q.net/post/elpriser-org-danish-electricity-prices/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/elpriser-org-danish-electricity-prices/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;elpriser.org&quot;&gt;elpriser.org&lt;&#x2F;a&gt; shows the &lt;strong&gt;real, all-in hourly price of electricity in Denmark&lt;&#x2F;strong&gt; — not just the spot price. It combines the Nord Pool spot price, your local grid tariff (&lt;code&gt;netselskab&lt;&#x2F;code&gt;), Energinet’s system and transmission tariffs, the state electricity tax (&lt;code&gt;elafgift&lt;&#x2F;code&gt;), and 25% VAT. Data comes from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.energidataservice.dk&#x2F;&quot;&gt;Energi Data Service&lt;&#x2F;a&gt; and updates daily after Nord Pool publishes the next 24 hours around 13:00. DK1 (Vestdanmark) and DK2 (Østdanmark). Danish-language, free, no login.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-does-danish-electricity-actually-cost&quot;&gt;What does Danish electricity actually cost?&lt;&#x2F;h2&gt;
&lt;p&gt;The “spot price” you see on most energy dashboards is only one component. The price you actually pay per kWh is made up of six parts:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Spot price&lt;&#x2F;strong&gt; — set hourly on the Nord Pool wholesale market for the following day.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Grid tariff (nettarif)&lt;&#x2F;strong&gt; — paid to your local grid operator (&lt;code&gt;netselskab&lt;&#x2F;code&gt;). Varies by company and by time-of-day (off-peak, shoulder, peak).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;System tariff (systemtarif)&lt;&#x2F;strong&gt; — paid to Energinet, the Danish TSO.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Transmission tariff (transmissionstarif)&lt;&#x2F;strong&gt; — also paid to Energinet.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Electricity tax (elafgift)&lt;&#x2F;strong&gt; — a state tax on consumption.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;VAT (moms)&lt;&#x2F;strong&gt; — 25% applied on top of everything else.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Most price sites show the first number. A few show one or two more. None that I could find showed all six, hour by hour, for both price zones, with the correct grid tariff automatically picked for the user’s &lt;code&gt;netselskab&lt;&#x2F;code&gt;. That’s what elpriser.org does.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-two-price-zones-dk1-and-dk2&quot;&gt;The two price zones: DK1 and DK2&lt;&#x2F;h2&gt;
&lt;p&gt;Denmark is split into two electricity price zones by geography — the Great Belt separates them — and they often have different prices at the same hour:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DK1 — Vestdanmark.&lt;&#x2F;strong&gt; Jylland and Fyn. More wind-dominated.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DK2 — Østdanmark.&lt;&#x2F;strong&gt; Sjælland, Lolland, Falster, Møn, Bornholm. More interconnected with Sweden (SE4) and Germany.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;When the wind blows hard in Jylland, DK1 can be cheap while DK2 is pricey. When an interconnector is down, the gap widens further. elpriser.org shows both zones side by side.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-netselskab-are-you-on&quot;&gt;Which netselskab are you on?&lt;&#x2F;h2&gt;
&lt;p&gt;You cannot choose your &lt;code&gt;netselskab&lt;&#x2F;code&gt; — it depends on where you live — but the grid tariff they charge is a significant chunk of your bill, and it varies by &lt;strong&gt;15–25 øre&#x2F;kWh at peak&lt;&#x2F;strong&gt; between the cheapest and the most expensive, which works out to &lt;strong&gt;~500–1,000 kr&#x2F;year&lt;&#x2F;strong&gt; for a typical household.&lt;&#x2F;p&gt;
&lt;p&gt;Rough ranking of peak (spidslast) tariffs as of 2025:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Zone&lt;&#x2F;th&gt;&lt;th&gt;Netselskab&lt;&#x2F;th&gt;&lt;th&gt;Peak tariff (øre&#x2F;kWh)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DK1&lt;&#x2F;td&gt;&lt;td&gt;RAH Net&lt;&#x2F;td&gt;&lt;td&gt;~33&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DK1&lt;&#x2F;td&gt;&lt;td&gt;Trefor&lt;&#x2F;td&gt;&lt;td&gt;~37&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DK1&lt;&#x2F;td&gt;&lt;td&gt;N1&lt;&#x2F;td&gt;&lt;td&gt;~46&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DK2&lt;&#x2F;td&gt;&lt;td&gt;Cerius&lt;&#x2F;td&gt;&lt;td&gt;~47&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DK2&lt;&#x2F;td&gt;&lt;td&gt;Radius&lt;&#x2F;td&gt;&lt;td&gt;~65&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Smaller operators (Konstant, Nord Energi, Vores Elnet, El-net Kongerslev, and others) each have their own tariffs. elpriser.org lets you pick yours; the all-in hourly price adjusts accordingly.&lt;&#x2F;p&gt;
&lt;p&gt;You can’t switch &lt;code&gt;netselskab&lt;&#x2F;code&gt;, but you &lt;strong&gt;can&lt;&#x2F;strong&gt; shift consumption — laundry, dishwasher, EV charging, heat-pump heating — into the low-tariff hours. The hourly chart exists for exactly that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;historical-extremes&quot;&gt;Historical extremes&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Record high:&lt;&#x2F;strong&gt; 16.69 kr&#x2F;kWh (spot price, excl. VAT) in DK2, at 19:00 on 5 September 2022, during the European energy crisis.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Record low:&lt;&#x2F;strong&gt; −2.76 kr&#x2F;kWh in DK1, at 14:00 on 2 July 2023, when wind production overshot demand.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Negative prices are not a bug: when production is high and demand is low, producers pay consumers to absorb the surplus. Modern price-aware heat pumps and EV chargers can lean into this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-elpriser-org-is-built&quot;&gt;How elpriser.org is built&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static HTML&lt;&#x2F;strong&gt;, regenerated once per day after Nord Pool’s next-day publication around 13:00 CET.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Data source&lt;&#x2F;strong&gt;: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.energidataservice.dk&#x2F;&quot;&gt;Energi Data Service&lt;&#x2F;a&gt;, the open-data service from Energinet. No API keys required.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Structured data&lt;&#x2F;strong&gt;: &lt;code&gt;schema.org&#x2F;WebApplication&lt;&#x2F;code&gt; and &lt;code&gt;FAQPage&lt;&#x2F;code&gt; are emitted on every page so Google can surface the prices directly in search results and AI overviews.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Hosting&lt;&#x2F;strong&gt;: a small origin behind Cloudflare. Cacheable for the full day; purged on the daily rebuild.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Language&lt;&#x2F;strong&gt;: Danish only. The domain name should have been a hint.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The whole thing is a single-purpose tool: no tracking, no login, no newsletter popover, no “10 ways to save on your electricity bill” listicles.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-often-does-elpriser-org-update&quot;&gt;How often does elpriser.org update?&lt;&#x2F;h3&gt;
&lt;p&gt;Once a day, after the Nord Pool day-ahead auction publishes next-day prices around 13:00 CET. Intraday adjustments are not shown; most consumers are billed against the day-ahead price anyway.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-the-price-shown-with-or-without-vat&quot;&gt;Is the price shown with or without VAT?&lt;&#x2F;h3&gt;
&lt;p&gt;With. The headline number on elpriser.org is the all-in price per kWh: spot + tariffs + tax + 25% VAT. You can toggle to see the breakdown.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-are-prices-in-dk1-and-dk2-different&quot;&gt;Why are prices in DK1 and DK2 different?&lt;&#x2F;h3&gt;
&lt;p&gt;Denmark has two physically separated electricity areas joined by the Great Belt interconnector. When the interconnector is fully used or wind production differs sharply between east and west, the two zones clear at different Nord Pool prices.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-see-the-breakdown-of-each-component&quot;&gt;Can I see the breakdown of each component?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes. Every hourly cell can be expanded to show spot price, each tariff component, elafgift, and VAT.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;do-i-really-pay-negative-prices-when-the-wholesale-price-goes-negative&quot;&gt;Do I really pay negative prices when the wholesale price goes negative?&lt;&#x2F;h3&gt;
&lt;p&gt;It depends on your contract. Pure spot-price contracts pass the raw Nord Pool price through — tariffs and tax still apply, so the all-in price can go slightly negative or just very low. Fixed-price contracts don’t.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-danish-only&quot;&gt;Why Danish only?&lt;&#x2F;h3&gt;
&lt;p&gt;The data, the regulatory rules, the &lt;code&gt;netselskab&lt;&#x2F;code&gt; structure, and the target audience are all Danish. An English version would be translating context, not content.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-does-the-data-come-from&quot;&gt;Where does the data come from?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.energidataservice.dk&#x2F;en&#x2F;dataset&#x2F;elspotprices&quot;&gt;Energi Data Service&lt;&#x2F;a&gt; (Nord Pool spot) and Energinet’s tariff data. Both are official public data sets from the Danish TSO.&lt;&#x2F;p&gt;
&lt;p&gt;Small, focused, no login, no tracking. The way consumer energy tools should look.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>winniemethmann.com — from WordPress to Astro</title>
        <published>2026-04-19T00:00:00+00:00</published>
        <updated>2026-04-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/winniemethmann-com-astro-portfolio/"/>
        <id>https://www.x2q.net/post/winniemethmann-com-astro-portfolio/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/winniemethmann-com-astro-portfolio/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;winniemethmann.com&quot;&gt;winniemethmann.com&lt;&#x2F;a&gt; is the portfolio of a Danish food photographer and recipe developer. It was a WordPress site for a decade. I rebuilt it on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;astro.build&quot;&gt;Astro&lt;&#x2F;a&gt;, using &lt;strong&gt;content collections&lt;&#x2F;strong&gt; (typed Markdown) instead of a CMS, &lt;strong&gt;Sharp + AVIF&lt;&#x2F;strong&gt; for the image pipeline, and &lt;strong&gt;Astro’s i18n routing&lt;&#x2F;strong&gt; (Danish default, English at &lt;code&gt;&#x2F;en&#x2F;&lt;&#x2F;code&gt;). Build time: ~30 seconds. Output size: ~70% smaller. No more admin panel, no more plugin updates, no more bot-probed login endpoint.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-move-off-wordpress&quot;&gt;Why move off WordPress&lt;&#x2F;h2&gt;
&lt;p&gt;For a portfolio that changes a few times a month, WordPress was doing too much work:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PHP runtime and MySQL&lt;&#x2F;strong&gt; for a site that is functionally static.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Dozens of plugins&lt;&#x2F;strong&gt; for image galleries, contact forms, SEO, caching — each with its own update cadence and security disclosures.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A half-forked theme&lt;&#x2F;strong&gt; that had accumulated patches nobody remembered why.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;An admin login endpoint&lt;&#x2F;strong&gt; that was probed several thousand times a day by bots.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Regular caching headaches&lt;&#x2F;strong&gt; every time the CDN and the plugins disagreed.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Concretely, dropping WordPress meant:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No admin panel to keep patched.&lt;&#x2F;strong&gt; WordPress core releases, plugin updates, theme updates — gone.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No database.&lt;&#x2F;strong&gt; Content lives as Markdown in the repo.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No login surface for bots to probe.&lt;&#x2F;strong&gt; There’s no &lt;code&gt;&#x2F;wp-admin&#x2F;&lt;&#x2F;code&gt; anymore.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;~10× faster page loads&lt;&#x2F;strong&gt; with no caching layer required.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;~70% smaller total build size&lt;&#x2F;strong&gt;, swapping hand-exported JPEGs for AVIF at sensible &lt;code&gt;srcset&lt;&#x2F;code&gt; breakpoints.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The tradeoff is that non-technical editing goes away. In practice that did not matter: the site owner was happier editing Markdown than fighting the WordPress block editor.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-astro&quot;&gt;Why Astro&lt;&#x2F;h2&gt;
&lt;p&gt;I considered Hugo, 11ty, Next.js static export, and SvelteKit. Astro won on three specific points:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Content collections.&lt;&#x2F;strong&gt; A typed, schema-checked way to describe a portfolio project as a directory of photos plus some front-matter. Build fails loudly if anything is malformed. No CMS required.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;&amp;lt;Image&amp;gt;&lt;&#x2F;code&gt; component.&lt;&#x2F;strong&gt; Astro’s built-in image pipeline handles AVIF + JPEG fallback + &lt;code&gt;srcset&lt;&#x2F;code&gt; + &lt;code&gt;width&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;height&lt;&#x2F;code&gt; attributes with a one-liner. Sharp is the engine under the hood.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Islands architecture, not relevant here.&lt;&#x2F;strong&gt; The site has no interactive components, so it ships basically zero JavaScript.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;content-collections-not-a-cms&quot;&gt;Content collections, not a CMS&lt;&#x2F;h2&gt;
&lt;p&gt;Each portfolio project is a directory under &lt;code&gt;src&#x2F;content&#x2F;portfolio&#x2F;&lt;&#x2F;code&gt; with a front-matter schema:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;src&#x2F;content&#x2F;portfolio&#x2F;
├── 2024-cookbook-editorial&#x2F;
│   ├── index.mdx
│   ├── cover.jpg
│   ├── 01.jpg, 02.jpg, …
└── 2023-spring-catalogue&#x2F;
    ├── index.mdx
    ├── cover.jpg
    └── 01.jpg …
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;index.mdx&lt;&#x2F;code&gt; front-matter declares the project title, category, year, and cover image. Astro enforces the schema at build time via &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.astro.build&#x2F;en&#x2F;guides&#x2F;content-collections&#x2F;&quot;&gt;&lt;code&gt;defineCollection&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; with a Zod schema. If a project is missing a cover image or has a bad category, the build fails.&lt;&#x2F;p&gt;
&lt;p&gt;Categories used: &lt;strong&gt;food photography, recipe development, interior &amp;amp; garden styling, editorial, cookbooks, and fashion&lt;&#x2F;strong&gt;. Adding a new project is &lt;code&gt;mkdir&lt;&#x2F;code&gt; + &lt;code&gt;cp *.jpg&lt;&#x2F;code&gt; + a short YAML front-matter block. No admin UI, no database migration, no cache to invalidate.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;image-pipeline-sharp-avif&quot;&gt;Image pipeline: Sharp + AVIF&lt;&#x2F;h2&gt;
&lt;p&gt;Food photography lives or dies on image quality. Astro’s &lt;code&gt;&amp;lt;Image&amp;gt;&lt;&#x2F;code&gt; component, powered by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sharp.pixelplumbing.com&#x2F;&quot;&gt;Sharp&lt;&#x2F;a&gt;, generates:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AVIF&lt;&#x2F;strong&gt; as the primary format. Roughly all modern browsers support it now (see &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caniuse.com&#x2F;avif&quot;&gt;caniuse&lt;&#x2F;a&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;JPEG&lt;&#x2F;strong&gt; fallback with matching &lt;code&gt;srcset&lt;&#x2F;code&gt; breakpoints (320, 640, 960, 1280, 1920 px).&lt;&#x2F;li&gt;
&lt;li&gt;Explicit &lt;code&gt;width&lt;&#x2F;code&gt; and &lt;code&gt;height&lt;&#x2F;code&gt; attributes, so there is zero cumulative layout shift while images load.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;loading=&quot;lazy&quot;&lt;&#x2F;code&gt; for below-the-fold images and &lt;code&gt;fetchpriority=&quot;high&quot;&lt;&#x2F;code&gt; for the hero.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Numbers&lt;&#x2F;strong&gt;: a typical portfolio page went from ~4.8 MB of hand-exported JPEGs to ~1.4 MB of AVIF — about a 70% reduction — with no visible quality loss at typical display sizes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;i18n-danish-default-english-at-en&quot;&gt;i18n — Danish default, English at &#x2F;en&#x2F;&lt;&#x2F;h2&gt;
&lt;p&gt;Astro’s i18n routing sits in &lt;code&gt;astro.config.mjs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;export default defineConfig({
  i18n: {
    defaultLocale: &amp;quot;da&amp;quot;,
    locales: [&amp;quot;da&amp;quot;, &amp;quot;en&amp;quot;],
    routing: { prefixDefaultLocale: false },
  },
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That gives:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&#x2F;&lt;&#x2F;code&gt; for Danish (the default, no prefix).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&#x2F;en&#x2F;&lt;&#x2F;code&gt; for English.&lt;&#x2F;li&gt;
&lt;li&gt;Each content entry declares its language via its collection and filename.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;link rel=&quot;alternate&quot; hreflang&amp;gt;&lt;&#x2F;code&gt; and the sitemap are generated from the same source.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Posts and portfolio entries that lack a translation simply don’t appear in the other locale’s routing — they don’t 404 to a wrong-language page.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deployment-and-build&quot;&gt;Deployment and build&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Output&lt;&#x2F;strong&gt;: fully static, deployed as plain files behind Cloudflare.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Build time&lt;&#x2F;strong&gt;: ~30 seconds on a cold cache, ~6 seconds incrementally.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No server, no database, no runtime cost.&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;measurable-outcomes&quot;&gt;Measurable outcomes&lt;&#x2F;h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;&#x2F;th&gt;&lt;th&gt;WordPress (before)&lt;&#x2F;th&gt;&lt;th&gt;Astro (after)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Build &#x2F; deploy time&lt;&#x2F;td&gt;&lt;td&gt;n&#x2F;a (instant publish)&lt;&#x2F;td&gt;&lt;td&gt;~30s cold, ~6s warm&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Typical page size (portfolio page)&lt;&#x2F;td&gt;&lt;td&gt;~4.8 MB&lt;&#x2F;td&gt;&lt;td&gt;~1.4 MB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Largest Contentful Paint&lt;&#x2F;td&gt;&lt;td&gt;~2.8 s&lt;&#x2F;td&gt;&lt;td&gt;~0.9 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Time to Interactive&lt;&#x2F;td&gt;&lt;td&gt;~3.5 s&lt;&#x2F;td&gt;&lt;td&gt;~1.1 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;JS shipped&lt;&#x2F;td&gt;&lt;td&gt;~220 KB&lt;&#x2F;td&gt;&lt;td&gt;~0 KB&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Plugins to keep patched&lt;&#x2F;td&gt;&lt;td&gt;14&lt;&#x2F;td&gt;&lt;td&gt;0&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;can-a-non-technical-owner-still-edit-the-site&quot;&gt;Can a non-technical owner still edit the site?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, for copy. Editing Markdown in a GitHub web editor is, in practice, easier than the WordPress block editor. For new portfolio projects, the workflow is: drag images into a folder, write a short front-matter block, commit. If that’s too technical, a one-page admin (Decap CMS &#x2F; Sveltia CMS &#x2F; Keystatic) can be bolted on.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-about-seo-after-the-migration&quot;&gt;What about SEO after the migration?&lt;&#x2F;h3&gt;
&lt;p&gt;URLs were kept as-is wherever possible. Missing old paths redirect via Cloudflare rules. The sitemap is regenerated on every build, and structured data (&lt;code&gt;Person&lt;&#x2F;code&gt;, &lt;code&gt;ImageGallery&lt;&#x2F;code&gt;, &lt;code&gt;Article&lt;&#x2F;code&gt;) is emitted per page.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-astro-and-not-hugo&quot;&gt;Why Astro and not Hugo?&lt;&#x2F;h3&gt;
&lt;p&gt;Hugo is faster and simpler for pure blogging, but Astro’s typed content collections and first-class &lt;code&gt;&amp;lt;Image&amp;gt;&lt;&#x2F;code&gt; component won this case. For &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.x2q.net&quot;&gt;x2q.net&lt;&#x2F;a&gt; itself, I later went with Zola — for a portfolio with a heavy image pipeline, Astro remained the better fit.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-images-stay-organised&quot;&gt;How do images stay organised?&lt;&#x2F;h3&gt;
&lt;p&gt;Each portfolio project owns its own folder. The Git repo is the CMS. &lt;code&gt;git log&lt;&#x2F;code&gt; tells you when an image was added and why.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-the-build-deterministic&quot;&gt;Is the build deterministic?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes. Given the same inputs, the output is byte-for-byte identical. Sharp is pinned in &lt;code&gt;package.json&lt;&#x2F;code&gt;; so is Astro. CI runs on Node LTS.&lt;&#x2F;p&gt;
&lt;p&gt;If you’re on a WordPress site that’s doing far less than its infrastructure suggests, Astro is worth the afternoon it takes to try.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>apextowww.com — a free apex-to-www redirect service</title>
        <published>2026-04-18T00:00:00+00:00</published>
        <updated>2026-04-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/apextowww-com-apex-to-www-redirect/"/>
        <id>https://www.x2q.net/post/apextowww-com-apex-to-www-redirect/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/apextowww-com-apex-to-www-redirect/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; DNS does not allow &lt;code&gt;CNAME&lt;&#x2F;code&gt; records at the zone apex (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rfc-editor.org&#x2F;rfc&#x2F;rfc1034#section-3.6.2&quot;&gt;RFC 1034 §3.6.2&lt;&#x2F;a&gt;). That’s why hosts like Netlify, Vercel, Cloudflare Pages, Firebase, and Heroku ask you to configure &lt;code&gt;www.example.com&lt;&#x2F;code&gt; with a &lt;code&gt;CNAME&lt;&#x2F;code&gt; and leave &lt;code&gt;example.com&lt;&#x2F;code&gt; (the apex) as a problem. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apextowww.com&quot;&gt;apextowww.com&lt;&#x2F;a&gt; solves it: point two &lt;code&gt;A&lt;&#x2F;code&gt; records and two &lt;code&gt;AAAA&lt;&#x2F;code&gt; records at the service, and it issues a Let’s Encrypt certificate for your apex and &lt;code&gt;301&lt;&#x2F;code&gt;-redirects every request to &lt;code&gt;https:&#x2F;&#x2F;www.yourdomain.tld&#x2F;&lt;&#x2F;code&gt;, preserving path and query string. Free, no signup, no account.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-you-can-t-put-a-cname-on-the-apex&quot;&gt;Why you can’t put a CNAME on the apex&lt;&#x2F;h2&gt;
&lt;p&gt;The DNS spec is unambiguous: a zone apex (the “bare” domain like &lt;code&gt;example.com&lt;&#x2F;code&gt;) must carry an &lt;code&gt;SOA&lt;&#x2F;code&gt; record and usually &lt;code&gt;NS&lt;&#x2F;code&gt; records. &lt;code&gt;CNAME&lt;&#x2F;code&gt; is defined as an alias that must be the only record at a name, and it is forbidden to coexist with the &lt;code&gt;SOA&lt;&#x2F;code&gt; and &lt;code&gt;NS&lt;&#x2F;code&gt; records that the apex is required to have. That’s why you can &lt;code&gt;CNAME www.example.com → mysite.netlify.app&lt;&#x2F;code&gt; but you &lt;strong&gt;cannot&lt;&#x2F;strong&gt; &lt;code&gt;CNAME example.com → mysite.netlify.app&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Workarounds exist, and all of them are a little awkward:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ALIAS &#x2F; ANAME records.&lt;&#x2F;strong&gt; Proprietary to each DNS provider (Cloudflare, Route 53, DNSimple, NS1). They work by resolving the target behind the scenes and returning an &lt;code&gt;A&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;AAAA&lt;&#x2F;code&gt;. Fine if your DNS provider supports it; useless if it doesn’t.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Flattening at the provider level.&lt;&#x2F;strong&gt; Cloudflare’s “CNAME flattening” is this, done automatically for you.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Your own always-on VPS doing &lt;code&gt;301 → www&lt;&#x2F;code&gt;.&lt;&#x2F;strong&gt; This is what I was doing for multiple domains, and it’s both over-engineered and a small maintenance tax.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;apextowww is the fourth option: somebody else’s always-on redirector, managed for you.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-apextowww-does&quot;&gt;What apextowww does&lt;&#x2F;h2&gt;
&lt;p&gt;Exactly one thing: a &lt;code&gt;301 Moved Permanently&lt;&#x2F;code&gt; from &lt;code&gt;https:&#x2F;&#x2F;yourdomain.tld&#x2F;anything?x=y&lt;&#x2F;code&gt; to &lt;code&gt;https:&#x2F;&#x2F;www.yourdomain.tld&#x2F;anything?x=y&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TLS&lt;&#x2F;strong&gt; is issued automatically on first request via the Let’s Encrypt HTTP-01 challenge. No ACME client to run yourself.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 + IPv6&lt;&#x2F;strong&gt; on dual-stack by design. Both apex records are required.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;HTTP&#x2F;1.1, HTTP&#x2F;2, and HTTP&#x2F;3&lt;&#x2F;strong&gt; are all served. The redirect itself is trivially small, so protocol version matters less, but it helps the first-paint story when the redirect is on the critical path.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Path and query string&lt;&#x2F;strong&gt; are preserved, so deep links keep working.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No signup, no login, no account.&lt;&#x2F;strong&gt; If your DNS is pointed correctly, it just works.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;how-to-set-it-up&quot;&gt;How to set it up&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apextowww.com&quot;&gt;apextowww.com&lt;&#x2F;a&gt; and copy the current IP addresses (two IPv4, two IPv6).&lt;&#x2F;li&gt;
&lt;li&gt;In your DNS provider, set &lt;code&gt;A&lt;&#x2F;code&gt; records on your apex pointing at the two IPv4 addresses. Remove any existing &lt;code&gt;A&lt;&#x2F;code&gt; record at the apex.&lt;&#x2F;li&gt;
&lt;li&gt;Set &lt;code&gt;AAAA&lt;&#x2F;code&gt; records on your apex pointing at the two IPv6 addresses.&lt;&#x2F;li&gt;
&lt;li&gt;Make sure &lt;code&gt;www.yourdomain.tld&lt;&#x2F;code&gt; still points at your real host (CNAME to Netlify&#x2F;Vercel&#x2F;Pages&#x2F;etc).&lt;&#x2F;li&gt;
&lt;li&gt;Wait for DNS to propagate. Visit &lt;code&gt;http:&#x2F;&#x2F;yourdomain.tld&#x2F;&lt;&#x2F;code&gt; — it should 301 to &lt;code&gt;https:&#x2F;&#x2F;www.yourdomain.tld&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The apextowww site has per-platform walkthroughs at &lt;code&gt;&#x2F;netlify-apex-domain-redirect&#x2F;&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;vercel-apex-domain-redirect&#x2F;&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;cloudflare-pages-apex-redirect&#x2F;&lt;&#x2F;code&gt;, &lt;code&gt;&#x2F;firebase-hosting-apex-redirect&#x2F;&lt;&#x2F;code&gt;, and &lt;code&gt;&#x2F;heroku-apex-domain-redirect&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;stack&quot;&gt;Stack&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hetzner ARM64&lt;&#x2F;strong&gt; servers, for cheap, low-power compute. ARM64 is ~30% cheaper per vCPU at Hetzner than x86 and runs the redirector fine.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Caddy-style automatic TLS&lt;&#x2F;strong&gt; with &lt;strong&gt;Let’s Encrypt&lt;&#x2F;strong&gt; HTTP-01 challenges.&lt;&#x2F;li&gt;
&lt;li&gt;Dual-stack &lt;strong&gt;IPv4 + IPv6&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;HTTP&#x2F;1.1, HTTP&#x2F;2, HTTP&#x2F;3&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Public &lt;strong&gt;static marketing site&lt;&#x2F;strong&gt; on Cloudflare Pages, with per-platform guides in separate URL paths so they each rank independently on Google for “netlify apex redirect”, “vercel apex domain”, etc.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;can-i-use-apextowww-with-cloudflare-dns&quot;&gt;Can I use apextowww with Cloudflare DNS?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, but you’ll want Cloudflare’s proxy (orange cloud) &lt;strong&gt;off&lt;&#x2F;strong&gt; for the apex &lt;code&gt;A&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;AAAA&lt;&#x2F;code&gt; records. If the proxy is on, Cloudflare intercepts the request and Let’s Encrypt’s HTTP-01 challenge won’t reach apextowww. Once the certificate is issued and renewed, you don’t need to toggle anything manually; just keep it off.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-apextowww-support-wildcards-or-redirecting-subdomains-other-than-www&quot;&gt;Does apextowww support wildcards or redirecting subdomains other than www?&lt;&#x2F;h3&gt;
&lt;p&gt;No. The service only redirects the apex to &lt;code&gt;www.&lt;&#x2F;code&gt;. Everything else stays on your real host.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-happens-if-the-apextowww-ips-change&quot;&gt;What happens if the apextowww IPs change?&lt;&#x2F;h3&gt;
&lt;p&gt;You’ll need to update your DNS &lt;code&gt;A&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;AAAA&lt;&#x2F;code&gt; records. The operator publishes current IPs on the homepage and gives advance notice for changes. For a personal domain this is fine; for anything mission-critical, run your own redirector.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-there-a-rate-limit&quot;&gt;Is there a rate limit?&lt;&#x2F;h3&gt;
&lt;p&gt;There’s no published hard limit, but this is a free community service. If you’re serving millions of apex redirects a day, self-host.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-not-just-use-cloudflare-s-free-plan&quot;&gt;Why not just use Cloudflare’s free plan?&lt;&#x2F;h3&gt;
&lt;p&gt;Cloudflare’s CNAME flattening at the apex is a perfectly reasonable alternative if your whole DNS is on Cloudflare. apextowww is useful when your DNS isn’t on Cloudflare, or when you want a host-agnostic redirector that works identically across domains.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-it-really-free&quot;&gt;Is it really free?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes. The marginal cost per redirect on ARM64 is negligible. No ads, no tracking beyond basic logs, no upsell.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-it-exists&quot;&gt;Why it exists&lt;&#x2F;h2&gt;
&lt;p&gt;Hosting platforms optimise for their own onboarding, not for the DNS truisms that trip up every new user. Sending people to a stranger’s VPS felt worse than running one myself. Now my own apex domains (including &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.x2q.net&quot;&gt;x2q.net&lt;&#x2F;a&gt;) point at apextowww, which means I could tear down the last little redirector VPS I still had running for historical reasons.&lt;&#x2F;p&gt;
&lt;p&gt;It’s the kind of project that isn’t interesting until you need it, and then it’s the only thing you need.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Print to PDF on Linux — set up a system-wide PDF printer with cups-pdf (2026)</title>
        <published>2013-01-30T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/print-to-pdf-on-linux/"/>
        <id>https://www.x2q.net/post/print-to-pdf-on-linux/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/print-to-pdf-on-linux/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; install &lt;strong&gt;&lt;code&gt;cups-pdf&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;, restart CUPS, and you get a virtual &lt;strong&gt;“PDF” printer&lt;&#x2F;strong&gt; that any application can print to. The files land in &lt;strong&gt;&lt;code&gt;~&#x2F;PDF&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; by default. On Debian&#x2F;Ubuntu that’s &lt;code&gt;sudo apt install printer-driver-cups-pdf &amp;amp;&amp;amp; sudo systemctl restart cups&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This post first went up here in 2013 as a two-line note — &lt;code&gt;apt-get install cups-pdf&lt;&#x2F;code&gt;, restart CUPS, done. The tool is still the right answer in 2026, so here is the current, fuller version.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;do-you-even-need-this&quot;&gt;Do you even need this?&lt;&#x2F;h2&gt;
&lt;p&gt;Most Linux desktop apps already print to PDF on their own. GTK and KDE print dialogs (so Firefox, Chrome, LibreOffice, GNOME apps, etc.) have a built-in &lt;strong&gt;“Print to File → PDF”&lt;&#x2F;strong&gt; option. If that covers you, you’re done.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;cups-pdf&lt;&#x2F;code&gt; earns its place when you want a &lt;strong&gt;real, system-wide printer queue&lt;&#x2F;strong&gt; rather than a per-app export:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Available to &lt;em&gt;every&lt;&#x2F;em&gt; application, including ones that have no PDF export of their own.&lt;&#x2F;li&gt;
&lt;li&gt;Usable from the &lt;strong&gt;command line&lt;&#x2F;strong&gt; and scripts.&lt;&#x2F;li&gt;
&lt;li&gt;Shareable &lt;strong&gt;over the network&lt;&#x2F;strong&gt; from a headless server, so other machines can “print to PDF” to a central spool.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;cups-pdf&lt;&#x2F;code&gt; is packaged on every mainstream distro:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Debian &#x2F; Ubuntu &#x2F; Mint &#x2F; Kali
sudo apt install printer-driver-cups-pdf      # older name: cups-pdf

# Fedora
sudo dnf install cups-pdf

# Arch &#x2F; Manjaro
sudo pacman -S cups-pdf
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then (re)start the CUPS daemon so it picks up the new backend:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart cups        # systemd
sudo service cups restart          # older sysvinit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The package’s post-install step normally registers the queue for you. That’s it — you can print to PDF now.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verify-the-printer-exists&quot;&gt;Verify the printer exists&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;$ lpstat -p -d
printer PDF is idle.  enabled since ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You’re looking for a queue named &lt;strong&gt;&lt;code&gt;PDF&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; (sometimes &lt;code&gt;Virtual_PDF_Printer&lt;&#x2F;code&gt;). If it isn’t there, add it by hand — first find the exact model string CUPS registered:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ lpinfo -m | grep -i pdf
CUPS-PDF_opt.ppd  Generic CUPS-PDF Printer (w&#x2F; options)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;…then create the queue with that model:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo lpadmin -p PDF -E -v cups-pdf:&#x2F; -m CUPS-PDF_opt.ppd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or do it in the GUI: &lt;strong&gt;Settings → Printers → Add&lt;&#x2F;strong&gt;, pick the &lt;em&gt;Generic CUPS-PDF&lt;&#x2F;em&gt; printer.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-the-files-go&quot;&gt;Where the files go&lt;&#x2F;h2&gt;
&lt;p&gt;By default, output lands in &lt;strong&gt;&lt;code&gt;~&#x2F;PDF&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; for the user who submitted the job. The location is set in &lt;code&gt;&#x2F;etc&#x2F;cups&#x2F;cups-pdf.conf&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$ grep -i &amp;#39;^Out\|^AnonDirName&amp;#39; &#x2F;etc&#x2F;cups&#x2F;cups-pdf.conf
Out ${HOME}&#x2F;PDF
AnonDirName &#x2F;var&#x2F;spool&#x2F;cups-pdf&#x2F;ANONYMOUS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Change the &lt;code&gt;Out&lt;&#x2F;code&gt; directive and &lt;code&gt;sudo systemctl restart cups&lt;&#x2F;code&gt; to send PDFs somewhere else. Jobs that arrive without a resolvable home directory (e.g. from a shared network queue) collect under &lt;code&gt;&#x2F;var&#x2F;spool&#x2F;cups-pdf&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;print-from-the-command-line&quot;&gt;Print from the command line&lt;&#x2F;h2&gt;
&lt;p&gt;Once the queue exists, it’s a normal CUPS printer:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;lp -d PDF report.txt          # any printable file
lpr -P PDF document.ps
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The resulting PDF shows up in &lt;code&gt;~&#x2F;PDF&lt;&#x2F;code&gt;. Set a default page size if the wrong one keeps appearing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;lpoptions -p PDF -o media=A4   # or media=Letter
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;headless-network-print-to-pdf&quot;&gt;Headless &#x2F; network “print to PDF”&lt;&#x2F;h2&gt;
&lt;p&gt;On a server with no desktop you can still run the whole thing: install &lt;code&gt;cups&lt;&#x2F;code&gt; + &lt;code&gt;cups-pdf&lt;&#x2F;code&gt;, enable the daemon, and share the queue.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now cups
sudo lpadmin -p PDF -o printer-is-shared=true
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With CUPS configured to listen on the network (&lt;code&gt;Listen&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Allow&lt;&#x2F;code&gt; in &lt;code&gt;&#x2F;etc&#x2F;cups&#x2F;cupsd.conf&lt;&#x2F;code&gt;), other machines can add this IPP printer and “print to PDF” remotely — the PDFs collect in the spool on the server. Handy for appliances and apps that only know how to talk to a network printer.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;nothing-appears-in-pdf&quot;&gt;Nothing appears in ~&#x2F;PDF&lt;&#x2F;h3&gt;
&lt;p&gt;Check the job actually completed and read the CUPS log:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;lpstat -W completed -o
sudo tail -n 50 &#x2F;var&#x2F;log&#x2F;cups&#x2F;error_log
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then confirm the &lt;code&gt;Out&lt;&#x2F;code&gt; path in &lt;code&gt;&#x2F;etc&#x2F;cups&#x2F;cups-pdf.conf&lt;&#x2F;code&gt; is what you expect.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;permission-denied-writing-the-pdf&quot;&gt;Permission denied writing the PDF&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;~&#x2F;PDF&lt;&#x2F;code&gt; has to be writable by the CUPS spool. If you print across a shared queue as another user, output lands under &lt;code&gt;&#x2F;var&#x2F;spool&#x2F;cups-pdf&#x2F;&amp;lt;user&amp;gt;&lt;&#x2F;code&gt; or &lt;code&gt;…&#x2F;ANONYMOUS&lt;&#x2F;code&gt; instead of your home directory.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fedora-selinux-blocks-the-write&quot;&gt;Fedora: SELinux blocks the write&lt;&#x2F;h3&gt;
&lt;p&gt;If a job silently disappears on Fedora, check for an SELinux denial (&lt;code&gt;sudo ausearch -m avc -ts recent&lt;&#x2F;code&gt;) — the &lt;code&gt;cups-pdf&lt;&#x2F;code&gt; backend writing to a non-default directory is the usual trigger.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;do-i-need-cups-pdf-if-i-use-gnome-firefox-or-chrome&quot;&gt;Do I need cups-pdf if I use GNOME, Firefox, or Chrome?&lt;&#x2F;h3&gt;
&lt;p&gt;Probably not — those export PDF directly from their print dialog. Reach for &lt;code&gt;cups-pdf&lt;&#x2F;code&gt; when you want one system-wide queue, command-line&#x2F;scripted output, or a network print-to-PDF server.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-print-to-pdf-on-windows-or-macos&quot;&gt;How do I print to PDF on Windows or macOS?&lt;&#x2F;h3&gt;
&lt;p&gt;You don’t need &lt;code&gt;cups-pdf&lt;&#x2F;code&gt;. Windows has a built-in &lt;strong&gt;“Microsoft Print to PDF”&lt;&#x2F;strong&gt; printer; macOS has a &lt;strong&gt;PDF&lt;&#x2F;strong&gt; dropdown in every print dialog. This post is Linux-only — and, as the 2013 original put it, setting this up on Linux is the easy case.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-s-the-difference-between-this-and-print-to-file&quot;&gt;What’s the difference between this and “Print to File”?&lt;&#x2F;h3&gt;
&lt;p&gt;“Print to File → PDF” in an app’s dialog is per-application and exports a single file. &lt;code&gt;cups-pdf&lt;&#x2F;code&gt; is a genuine CUPS print queue: shared across apps, scriptable, and shareable over the network.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;which-package-name-cups-pdf-or-printer-driver-cups-pdf&quot;&gt;Which package name — cups-pdf or printer-driver-cups-pdf?&lt;&#x2F;h3&gt;
&lt;p&gt;On current Debian&#x2F;Ubuntu the package is &lt;code&gt;printer-driver-cups-pdf&lt;&#x2F;code&gt;; &lt;code&gt;cups-pdf&lt;&#x2F;code&gt; is kept as a transitional name and still works. Fedora and Arch call it &lt;code&gt;cups-pdf&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo apt install printer-driver-cups-pdf&lt;&#x2F;code&gt; (or your distro’s equivalent), then &lt;code&gt;sudo systemctl restart cups&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;A virtual &lt;strong&gt;PDF&lt;&#x2F;strong&gt; printer appears; verify with &lt;code&gt;lpstat -p -d&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Output defaults to &lt;strong&gt;&lt;code&gt;~&#x2F;PDF&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;, configurable via &lt;code&gt;Out&lt;&#x2F;code&gt; in &lt;code&gt;&#x2F;etc&#x2F;cups&#x2F;cups-pdf.conf&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Print from any app’s dialog, from the CLI with &lt;code&gt;lp -d PDF file&lt;&#x2F;code&gt;, or over the network from a headless box.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Create a real multi-resolution favicon.ico (2026)</title>
        <published>2013-01-13T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/multi-resolution-favicon-ico/"/>
        <id>https://www.x2q.net/post/multi-resolution-favicon-ico/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/multi-resolution-favicon-ico/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; a &lt;code&gt;.ico&lt;&#x2F;code&gt; file can contain &lt;em&gt;multiple&lt;&#x2F;em&gt; image sizes in one file. Render 16&#x2F;32&#x2F;48px PNGs from a high-res source and combine them: &lt;code&gt;magick 16.png 32.png 48.png favicon.ico&lt;&#x2F;code&gt;. Then add a modern &lt;code&gt;favicon.svg&lt;&#x2F;code&gt; and an &lt;code&gt;apple-touch-icon.png&lt;&#x2F;code&gt; for everything else.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This first went up here in 2013 with GIMP-by-hand instructions. The multi-resolution &lt;code&gt;.ico&lt;&#x2F;code&gt; trick still matters, but in 2026 the command-line route is faster and an &lt;strong&gt;SVG favicon&lt;&#x2F;strong&gt; does most of the heavy lifting — both are below.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;why-multi-resolution&quot;&gt;Why multi-resolution&lt;&#x2F;h2&gt;
&lt;p&gt;Most favicons are exported at a single 16×16 resolution. That looks fine in a tab but pixelated when a browser scales it up — bookmarks bars, the address bar on hi-DPI screens, pinned tabs, OS shortcuts. Browsers ask for different sizes (16, 32, 48…), and the &lt;code&gt;.ico&lt;&#x2F;code&gt; container can hold all of them in one file, so the browser picks the best fit.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-command-line-way-imagemagick&quot;&gt;The command-line way (ImageMagick)&lt;&#x2F;h2&gt;
&lt;p&gt;Start from a square source image, &lt;strong&gt;at least 256×256&lt;&#x2F;strong&gt;. Rasterize the sizes you want, then pack them into one &lt;code&gt;.ico&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;From a PNG source:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;magick source.png -resize 16x16   &#x2F;tmp&#x2F;16.png
magick source.png -resize 32x32   &#x2F;tmp&#x2F;32.png
magick source.png -resize 48x48   &#x2F;tmp&#x2F;48.png
magick &#x2F;tmp&#x2F;16.png &#x2F;tmp&#x2F;32.png &#x2F;tmp&#x2F;48.png favicon.ico
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;ImageMagick’s own SVG rasterizer is flaky — if your source is an SVG, render the PNGs with &lt;code&gt;rsvg-convert&lt;&#x2F;code&gt; first, then combine:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rsvg-convert -w 16 -h 16 logo.svg -o &#x2F;tmp&#x2F;16.png
rsvg-convert -w 32 -h 32 logo.svg -o &#x2F;tmp&#x2F;32.png
rsvg-convert -w 48 -h 48 logo.svg -o &#x2F;tmp&#x2F;48.png
magick &#x2F;tmp&#x2F;16.png &#x2F;tmp&#x2F;32.png &#x2F;tmp&#x2F;48.png favicon.ico
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Verify the &lt;code&gt;.ico&lt;&#x2F;code&gt; really contains all three sizes:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;magick identify favicon.ico
favicon.ico[0] ICO 16x16 ...
favicon.ico[1] ICO 32x32 ...
favicon.ico[2] ICO 48x48 ...
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;the-gimp-way-no-imagemagick&quot;&gt;The GIMP way (no ImageMagick)&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Open your square, high-res source in GIMP.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Image → Canvas Size&lt;&#x2F;strong&gt; to make it perfectly square if it isn’t.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Image → Scale Image&lt;&#x2F;strong&gt; to 48×48, then &lt;strong&gt;Layer → Duplicate&lt;&#x2F;strong&gt; and scale copies to 32×32 and 16×16, so you have one layer per size.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;File → Export As → &lt;code&gt;favicon.ico&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;. GIMP writes each layer as a resolution inside the single &lt;code&gt;.ico&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;the-modern-half-svg-apple-touch-icon&quot;&gt;The modern half: SVG + apple-touch-icon&lt;&#x2F;h2&gt;
&lt;p&gt;In 2026 the &lt;code&gt;.ico&lt;&#x2F;code&gt; is the legacy fallback. Modern browsers prefer a crisp, scalable &lt;strong&gt;SVG favicon&lt;&#x2F;strong&gt;, and iOS wants a PNG &lt;strong&gt;apple-touch-icon&lt;&#x2F;strong&gt;. Ship all three:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;rsvg-convert -w 180 -h 180 -b &amp;quot;#fff&amp;quot; logo.svg -o apple-touch-icon.png
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;link rel=&amp;quot;icon&amp;quot; href=&amp;quot;&#x2F;favicon.ico&amp;quot; sizes=&amp;quot;32x32&amp;quot;&amp;gt;
&amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image&#x2F;svg+xml&amp;quot; href=&amp;quot;&#x2F;favicon.svg&amp;quot;&amp;gt;
&amp;lt;link rel=&amp;quot;apple-touch-icon&amp;quot; href=&amp;quot;&#x2F;apple-touch-icon.png&amp;quot;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;sizes=&quot;32x32&quot;&lt;&#x2F;code&gt; hint on the &lt;code&gt;.ico&lt;&#x2F;code&gt; tells modern browsers “there’s something better here” so they reach for the SVG, while older ones fall back to the multi-resolution &lt;code&gt;.ico&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;do-i-still-need-favicon-ico-if-i-have-an-svg&quot;&gt;Do I still need favicon.ico if I have an SVG?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes, as a fallback. Older browsers and a lot of link-preview&#x2F;scraper bots only look for &lt;code&gt;&#x2F;favicon.ico&lt;&#x2F;code&gt; at the site root, so keep it there.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-sizes-should-the-ico-contain&quot;&gt;What sizes should the .ico contain?&lt;&#x2F;h3&gt;
&lt;p&gt;16, 32, and 48 cover essentially everything in practice. You &lt;em&gt;can&lt;&#x2F;em&gt; add 64 and 128, but they mostly just inflate the file — the SVG handles large sizes better.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-does-my-favicon-not-update&quot;&gt;Why does my favicon not update?&lt;&#x2F;h3&gt;
&lt;p&gt;Browsers cache favicons aggressively. Hard-refresh, or append a one-off query string while testing (&lt;code&gt;&#x2F;favicon.ico?v=2&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;.ico&lt;&#x2F;code&gt; holds multiple sizes; bake in 16&#x2F;32&#x2F;48 with &lt;code&gt;magick 16.png 32.png 48.png favicon.ico&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Rasterize SVG sources with &lt;code&gt;rsvg-convert&lt;&#x2F;code&gt; first (ImageMagick’s SVG support is unreliable).&lt;&#x2F;li&gt;
&lt;li&gt;Pair the &lt;code&gt;.ico&lt;&#x2F;code&gt; with a modern &lt;code&gt;favicon.svg&lt;&#x2F;code&gt; + &lt;code&gt;apple-touch-icon.png&lt;&#x2F;code&gt; and three &lt;code&gt;&amp;lt;link&amp;gt;&lt;&#x2F;code&gt; tags.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Pretty-print XML on the command line with xmllint (2026)</title>
        <published>2012-11-20T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/pretty-print-xml-xmllint/"/>
        <id>https://www.x2q.net/post/pretty-print-xml-xmllint/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/pretty-print-xml-xmllint/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;xmllint --format file.xml&lt;&#x2F;code&gt; pretty-prints XML. Read from a pipe with &lt;code&gt;-&lt;&#x2F;code&gt;, control indentation with &lt;code&gt;XMLLINT_INDENT&lt;&#x2F;code&gt;, and write back with &lt;code&gt;--output&lt;&#x2F;code&gt;. It ships with &lt;strong&gt;libxml2&lt;&#x2F;strong&gt;, so it’s almost certainly already installed.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This one first went up here in 2012 — I was poking at a Visa &lt;strong&gt;3-D Secure&lt;&#x2F;strong&gt; test directory that answered in single-line XML and needed it readable. The tool hasn’t changed; the flags below are the current, double-dash forms.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h2&gt;
&lt;p&gt;API responses come back as one unreadable line. Here’s the kind of thing a payment directory server hands you:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;&amp;lt;ThreeDSecure&amp;gt;&amp;lt;Message&amp;gt;&amp;lt;VERes&amp;gt;&amp;lt;version&amp;gt;1.0.2&amp;lt;&#x2F;version&amp;gt;&amp;lt;CH&amp;gt;&amp;lt;enrolled&amp;gt;Y&amp;lt;&#x2F;enrolled&amp;gt;&amp;lt;&#x2F;CH&amp;gt;&amp;lt;url&amp;gt;https:&#x2F;&#x2F;acs.example&#x2F;PIT&#x2F;ACS&amp;lt;&#x2F;url&amp;gt;&amp;lt;&#x2F;VERes&amp;gt;&amp;lt;&#x2F;Message&amp;gt;&amp;lt;&#x2F;ThreeDSecure&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;format-a-file&quot;&gt;Format a file&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;xmllint --format response.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That writes the indented version to stdout. To overwrite (or write a new file):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xmllint --format response.xml --output response.pretty.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;format-a-stream-the-useful-one&quot;&gt;Format a stream (the useful one)&lt;&#x2F;h2&gt;
&lt;p&gt;Pass &lt;code&gt;-&lt;&#x2F;code&gt; as the filename to read stdin, so you can pretty-print straight from &lt;code&gt;curl&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;curl -s https:&#x2F;&#x2F;api.example&#x2F;thing | xmllint --format -
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;ThreeDSecure&amp;gt;
  &amp;lt;Message&amp;gt;
    &amp;lt;VERes&amp;gt;
      &amp;lt;version&amp;gt;1.0.2&amp;lt;&#x2F;version&amp;gt;
      &amp;lt;CH&amp;gt;
        &amp;lt;enrolled&amp;gt;Y&amp;lt;&#x2F;enrolled&amp;gt;
      &amp;lt;&#x2F;CH&amp;gt;
      &amp;lt;url&amp;gt;https:&#x2F;&#x2F;acs.example&#x2F;PIT&#x2F;ACS&amp;lt;&#x2F;url&amp;gt;
    &amp;lt;&#x2F;VERes&amp;gt;
  &amp;lt;&#x2F;Message&amp;gt;
&amp;lt;&#x2F;ThreeDSecure&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;control-the-indentation&quot;&gt;Control the indentation&lt;&#x2F;h2&gt;
&lt;p&gt;By default &lt;code&gt;xmllint&lt;&#x2F;code&gt; indents with two spaces. Override it with the &lt;code&gt;XMLLINT_INDENT&lt;&#x2F;code&gt; environment variable — set it to spaces or a tab:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;XMLLINT_INDENT=&amp;quot;    &amp;quot; xmllint --format response.xml     # 4 spaces
XMLLINT_INDENT=$&amp;#39;\t&amp;#39;   xmllint --format response.xml     # tabs
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;recover-broken-xml&quot;&gt;Recover broken XML&lt;&#x2F;h2&gt;
&lt;p&gt;If the document is slightly malformed (a stray entity, a missing close tag from a truncated download), add &lt;code&gt;--recover&lt;&#x2F;code&gt; to format what it can instead of bailing:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xmllint --recover --format broken.xml
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;while-you-re-there-validate&quot;&gt;While you’re there: validate&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;xmllint&lt;&#x2F;code&gt; isn’t just a formatter. Check well-formedness (exit code 0 = OK), or validate against a schema:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;xmllint --noout response.xml                       # well-formed?
xmllint --noout --schema schema.xsd response.xml   # valid against XSD?
xmllint --noout --dtdvalid doc.dtd response.xml    # valid against DTD?
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;install-if-it-s-somehow-missing&quot;&gt;Install (if it’s somehow missing)&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install libxml2-utils     # Debian &#x2F; Ubuntu
sudo dnf install libxml2           # Fedora
brew install libxml2               # macOS (then use the keg&amp;#39;s bin)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-do-i-minify-xml-instead-of-pretty-printing-it&quot;&gt;How do I minify XML instead of pretty-printing it?&lt;&#x2F;h3&gt;
&lt;p&gt;Use &lt;code&gt;--noblanks&lt;&#x2F;code&gt;: &lt;code&gt;xmllint --noblanks file.xml&lt;&#x2F;code&gt; strips the insignificant whitespace.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-extract-a-single-value-instead-of-formatting-the-whole-thing&quot;&gt;Can I extract a single value instead of formatting the whole thing?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — &lt;code&gt;xmllint --xpath &#x27;&#x2F;&#x2F;url&#x2F;text()&#x27; response.xml&lt;&#x2F;code&gt; pulls out a node with XPath. Handy for scripting against XML APIs.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-about-json&quot;&gt;What about JSON?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;xmllint&lt;&#x2F;code&gt; is XML-only. For JSON, &lt;code&gt;jq .&lt;&#x2F;code&gt; is the equivalent (“&lt;code&gt;jq&lt;&#x2F;code&gt; for XML” is roughly &lt;code&gt;xmllint --format&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xmllint --format file.xml&lt;&#x2F;code&gt; — pretty-print a file.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;… | xmllint --format -&lt;&#x2F;code&gt; — pretty-print a stream.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;XMLLINT_INDENT&lt;&#x2F;code&gt; — set the indent.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--recover&lt;&#x2F;code&gt;, &lt;code&gt;--noout --schema&lt;&#x2F;code&gt;, &lt;code&gt;--xpath&lt;&#x2F;code&gt; — fix, validate, and query.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Mount a BIN&#x2F;CUE disc image on Linux (2026)</title>
        <published>2012-10-16T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/mount-bin-cue-linux/"/>
        <id>https://www.x2q.net/post/mount-bin-cue-linux/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/mount-bin-cue-linux/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; you can’t &lt;code&gt;mount&lt;&#x2F;code&gt; a BIN&#x2F;CUE pair straight away. Convert it: &lt;code&gt;bchunk image.bin image.cue out&lt;&#x2F;code&gt; produces an ISO you can loop-mount. For discs with audio tracks or copy protection, use &lt;strong&gt;cdemu&lt;&#x2F;strong&gt; to emulate a drive instead.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2012 note, still accurate. BIN&#x2F;CUE images are everywhere (old game&#x2F;VCD rips), and the loop-mount confusion is perennial. See also the companion post on &lt;a href=&quot;&#x2F;post&#x2F;bchunk-bin-cue-to-iso-wav&#x2F;&quot;&gt;bchunk itself&lt;&#x2F;a&gt; if you just need the conversion.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;why-you-can-t-just-mount-it&quot;&gt;Why you can’t just mount it&lt;&#x2F;h2&gt;
&lt;p&gt;A &lt;code&gt;.bin&lt;&#x2F;code&gt; is a raw CD sector dump and the &lt;code&gt;.cue&lt;&#x2F;code&gt; is a text sheet describing its track layout. The kernel’s loopback mounter understands filesystem images (ISO9660, etc.), not raw sector dumps with a separate cue sheet — so &lt;code&gt;mount image.bin&lt;&#x2F;code&gt; fails. You bridge the gap one of two ways.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;option-1-convert-to-iso-with-bchunk-data-discs&quot;&gt;Option 1 — convert to ISO with bchunk (data discs)&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt; (binchunker) converts a &lt;code&gt;.bin&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.cue&lt;&#x2F;code&gt; set into &lt;code&gt;.iso&lt;&#x2F;code&gt; (data) and &lt;code&gt;.cdr&lt;&#x2F;code&gt; (audio) tracks.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install bchunk          # Debian &#x2F; Ubuntu
sudo dnf install bchunk          # Fedora
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Convert — the last argument is the output &lt;em&gt;basename&lt;&#x2F;em&gt;, so this writes &lt;code&gt;out01.iso&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;bchunk image.bin image.cue out
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then loop-mount the ISO:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo mount -o loop,ro -t iso9660 out01.iso &#x2F;mnt
ls &#x2F;mnt
sudo umount &#x2F;mnt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the right tool when the image is a single data track — most software&#x2F;VCD rips.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;option-2-emulate-a-drive-with-cdemu-audio-mixed-mode&quot;&gt;Option 2 — emulate a drive with cdemu (audio &#x2F; mixed-mode)&lt;&#x2F;h2&gt;
&lt;p&gt;If the disc has audio tracks, multiple sessions, or anything bchunk’s flat conversion would mangle, emulate a real optical drive with &lt;strong&gt;cdemu&lt;&#x2F;strong&gt;. It loads the &lt;code&gt;.cue&lt;&#x2F;code&gt; directly and exposes a virtual &lt;code&gt;&#x2F;dev&#x2F;sr&lt;&#x2F;code&gt; device the desktop auto-mounts.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install cdemu-client gcdemu      # Debian &#x2F; Ubuntu (universe)

cdemu load 0 image.cue                     # load into virtual drive 0
cdemu status
# the virtual disc now appears like a real one; eject with:
cdemu unload 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;cdemu handles audio and mixed-mode discs that a straight ISO conversion can’t represent.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;option-3-just-extract-the-files&quot;&gt;Option 3 — just extract the files&lt;&#x2F;h2&gt;
&lt;p&gt;If you don’t need a mounted volume, &lt;strong&gt;7-Zip&lt;&#x2F;strong&gt; can list and extract straight from many BIN&#x2F;CUE data images:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;7z l image.cue      # list contents
7z x image.cue      # extract here
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;bchunk-wrote-a-iso-and-several-cdr-files-what-are-those&quot;&gt;bchunk wrote a .iso and several .cdr files — what are those?&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;.cdr&lt;&#x2F;code&gt; files are the raw audio tracks. Convert one to WAV with &lt;code&gt;sox track.cdr track.wav&lt;&#x2F;code&gt; (it’s headerless CD-DA), or ignore them if you only wanted the data track.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;mount-says-wrong-fs-type-on-the-converted-iso&quot;&gt;mount says “wrong fs type” on the converted ISO&lt;&#x2F;h3&gt;
&lt;p&gt;The image may not be ISO9660 (e.g. a UDF or game-console format). Drop the &lt;code&gt;-t iso9660&lt;&#x2F;code&gt; and let the kernel autodetect: &lt;code&gt;sudo mount -o loop,ro out01.iso &#x2F;mnt&lt;&#x2F;code&gt;. Console images often need a dedicated emulator instead.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-do-this-on-macos&quot;&gt;Can I do this on macOS?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;bchunk&lt;&#x2F;code&gt; is in Homebrew (&lt;code&gt;brew install bchunk&lt;&#x2F;code&gt;); convert to ISO, then &lt;code&gt;hdiutil attach out01.iso&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;BIN&#x2F;CUE won’t loop-mount directly.&lt;&#x2F;li&gt;
&lt;li&gt;Data disc → &lt;code&gt;bchunk image.bin image.cue out&lt;&#x2F;code&gt; then &lt;code&gt;mount -o loop out01.iso &#x2F;mnt&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Audio&#x2F;mixed-mode → load the &lt;code&gt;.cue&lt;&#x2F;code&gt; in &lt;strong&gt;cdemu&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Just need the files → &lt;code&gt;7z x image.cue&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sudo without a password on Ubuntu&#x2F;Debian — safely (2026)</title>
        <published>2012-10-11T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/sudo-without-password/"/>
        <id>https://www.x2q.net/post/sudo-without-password/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/sudo-without-password/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; don’t edit &lt;code&gt;&#x2F;etc&#x2F;sudoers&lt;&#x2F;code&gt; directly. Create a validated drop-in: &lt;code&gt;sudo visudo -f &#x2F;etc&#x2F;sudoers.d&#x2F;nopasswd&lt;&#x2F;code&gt; and add &lt;code&gt;youruser ALL=(ALL) NOPASSWD: ALL&lt;&#x2F;code&gt;. Better yet, scope it to the &lt;em&gt;specific commands&lt;&#x2F;em&gt; you actually need to run unattended.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2012 note, refreshed. The mechanism is unchanged, but in 2026 the right move is a drop-in file under &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;&lt;&#x2F;code&gt; (not editing the main file), and scoping the rule rather than handing out blanket root.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;always-use-visudo&quot;&gt;Always use visudo&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;sudo&lt;&#x2F;code&gt; reads &lt;code&gt;&#x2F;etc&#x2F;sudoers&lt;&#x2F;code&gt; and every file in &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;&lt;&#x2F;code&gt;. &lt;strong&gt;&lt;code&gt;visudo&lt;&#x2F;code&gt; syntax-checks before saving&lt;&#x2F;strong&gt; — a typo in &lt;code&gt;sudoers&lt;&#x2F;code&gt; can lock you out of &lt;code&gt;sudo&lt;&#x2F;code&gt; entirely, so never edit these files with a plain editor.&lt;&#x2F;p&gt;
&lt;p&gt;Edit a dedicated drop-in (keeps your change out of the distro-managed main file):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo visudo -f &#x2F;etc&#x2F;sudoers.d&#x2F;nopasswd
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;single-user&quot;&gt;Single user&lt;&#x2F;h2&gt;
&lt;p&gt;Add this line (swap in your username):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;alice ALL=(ALL) NOPASSWD: ALL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;a-group-instead&quot;&gt;A group instead&lt;&#x2F;h2&gt;
&lt;p&gt;On Ubuntu, members of the &lt;code&gt;sudo&lt;&#x2F;code&gt; group get admin rights (it’s &lt;code&gt;wheel&lt;&#x2F;code&gt; on Fedora&#x2F;RHEL). Make the whole group passwordless:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;%sudo ALL=(ALL) NOPASSWD: ALL
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;the-version-you-should-actually-use-scope-it&quot;&gt;The version you should actually use: scope it&lt;&#x2F;h2&gt;
&lt;p&gt;Blanket &lt;code&gt;NOPASSWD: ALL&lt;&#x2F;code&gt; means anything that can run as that user can now become root with no further check — a real escalation risk. For automation, grant only the commands the job needs:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# deploy user may restart one service and nothing else, no password
deploy ALL=(ALL) NOPASSWD: &#x2F;usr&#x2F;bin&#x2F;systemctl restart myapp, &#x2F;usr&#x2F;bin&#x2F;systemctl status myapp
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the difference between “convenient” and “convenient &lt;em&gt;and&lt;&#x2F;em&gt; defensible”.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;apply-and-test&quot;&gt;Apply and test&lt;&#x2F;h2&gt;
&lt;p&gt;Files in &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;&lt;&#x2F;code&gt; are picked up immediately — no reload needed. Validate the whole config and test in a way that won’t strand you:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo visudo -c                 # syntax-check every sudoers file
sudo -k                        # clear the cached credential
sudo -n true &amp;amp;&amp;amp; echo &amp;quot;passwordless works&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Keep your current root shell open until you’ve confirmed it works in a &lt;em&gt;second&lt;&#x2F;em&gt; terminal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Filenames matter.&lt;&#x2F;strong&gt; &lt;code&gt;sudo&lt;&#x2F;code&gt; ignores files in &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;&lt;&#x2F;code&gt; with a &lt;code&gt;.&lt;&#x2F;code&gt; or &lt;code&gt;~&lt;&#x2F;code&gt; in the name (so &lt;code&gt;nopasswd.conf&lt;&#x2F;code&gt; is silently skipped). Use a plain name like &lt;code&gt;nopasswd&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Permissions matter.&lt;&#x2F;strong&gt; The file must be &lt;code&gt;0440&lt;&#x2F;code&gt; and owned by root; &lt;code&gt;visudo -f&lt;&#x2F;code&gt; sets this for you. A wrong mode makes sudo refuse to start.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Order matters.&lt;&#x2F;strong&gt; Later rules win. A broad &lt;code&gt;NOPASSWD: ALL&lt;&#x2F;code&gt; after a narrow rule re-opens everything.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-passwordless-sudo-a-security-risk&quot;&gt;Is passwordless sudo a security risk?&lt;&#x2F;h3&gt;
&lt;p&gt;Blanket &lt;code&gt;NOPASSWD: ALL&lt;&#x2F;code&gt; removes the last speed-bump between a compromised user account and root. Fine on a throwaway lab VM; scope it (or skip it) on anything that matters. Command-scoped rules are the compromise.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-require-a-password-again&quot;&gt;How do I require a password again?&lt;&#x2F;h3&gt;
&lt;p&gt;Delete the drop-in (&lt;code&gt;sudo rm &#x2F;etc&#x2F;sudoers.d&#x2F;nopasswd&lt;&#x2F;code&gt;) or remove the &lt;code&gt;NOPASSWD:&lt;&#x2F;code&gt; keyword from the line.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-does-it-still-ask-for-a-password&quot;&gt;Why does it still ask for a password?&lt;&#x2F;h3&gt;
&lt;p&gt;Another rule later in the chain overrides yours, the filename contains a &lt;code&gt;.&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;~&lt;&#x2F;code&gt; (so it’s ignored), or you edited &lt;code&gt;&#x2F;etc&#x2F;sudoers&lt;&#x2F;code&gt; but a drop-in re-requires it. Run &lt;code&gt;sudo visudo -c&lt;&#x2F;code&gt; and check &lt;code&gt;&#x2F;etc&#x2F;sudoers.d&#x2F;&lt;&#x2F;code&gt; for conflicting files.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Edit a drop-in with &lt;code&gt;sudo visudo -f &#x2F;etc&#x2F;sudoers.d&#x2F;nopasswd&lt;&#x2F;code&gt; — never the main file by hand.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;user ALL=(ALL) NOPASSWD: ALL&lt;&#x2F;code&gt; for a user, &lt;code&gt;%sudo …&lt;&#x2F;code&gt; for a group.&lt;&#x2F;li&gt;
&lt;li&gt;Scope to specific commands for anything beyond a lab box. Validate with &lt;code&gt;visudo -c&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>View the contents of a CSR with OpenSSL (2026)</title>
        <published>2010-07-04T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/view-csr-contents-openssl/"/>
        <id>https://www.x2q.net/post/view-csr-contents-openssl/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/view-csr-contents-openssl/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;openssl req -in host.csr -noout -text -verify&lt;&#x2F;code&gt; decodes a certificate signing request into human-readable form and checks its signature. Use it to confirm the CN, the SANs, and the key before you hand the CSR to a CA.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is one of the oldest notes on this blog (2010). OpenSSL’s CLI is famously sprawling, and “how do I just &lt;em&gt;read&lt;&#x2F;em&gt; a CSR” is a question that never goes away — so here’s the refreshed version.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;what-a-csr-is&quot;&gt;What a CSR is&lt;&#x2F;h2&gt;
&lt;p&gt;A &lt;strong&gt;Certificate Signing Request (CSR)&lt;&#x2F;strong&gt; is what you send to a Certificate Authority (CA) to be signed. It bundles your public key plus the identity you’re requesting (the subject: common name, organisation, and — the part that actually matters now — the &lt;strong&gt;Subject Alternative Names&lt;&#x2F;strong&gt;), all signed by your private key. The CA signs it and hands back a certificate.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;read-it&quot;&gt;Read it&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;openssl req -in host.csr -noout -text -verify
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-in host.csr&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — the request file.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-noout&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — don’t re-print the encoded CSR, just the decoded info.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-text&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — human-readable output.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-verify&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — check the CSR’s self-signature (catches a corrupted or tampered request).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;You’ll get something like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C=DK, ST=Capital, L=Copenhagen, O=Example ApS, CN=example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
        Requested Extensions:
            X509v3 Subject Alternative Name:
                DNS:example.com, DNS:www.example.com
    Signature Algorithm: sha256WithRSAEncryption
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Check three things before sending it off: the &lt;strong&gt;CN&#x2F;subject&lt;&#x2F;strong&gt; is right, the &lt;strong&gt;SANs&lt;&#x2F;strong&gt; list every hostname the cert must cover (modern browsers ignore the CN and only trust SANs), and the &lt;strong&gt;key size&#x2F;algorithm&lt;&#x2F;strong&gt; meets the CA’s minimum (2048-bit RSA or an EC key).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pull-out-just-one-field&quot;&gt;Pull out just one field&lt;&#x2F;h2&gt;
&lt;p&gt;When you only need the subject or the SANs (e.g. in a script):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -in host.csr -noout -subject
openssl req -in host.csr -noout -text | grep -A1 &amp;quot;Subject Alternative Name&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;verify-a-csr-matches-its-private-key&quot;&gt;Verify a CSR matches its private key&lt;&#x2F;h2&gt;
&lt;p&gt;A common cutover mistake is generating the CSR from the wrong key. The modulus (RSA) of the CSR, the key, and the eventual certificate must all match. Compare hashes:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl req  -in host.csr      -noout -modulus | openssl md5
openssl rsa  -in host.key      -noout -modulus | openssl md5
openssl x509 -in host.crt      -noout -modulus | openssl md5
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;All three digests identical → they belong together. Any mismatch → you’ve got the wrong key (or wrong cert) and TLS will fail to start.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;generate-a-csr-for-completeness&quot;&gt;Generate a CSR (for completeness)&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;openssl req -new -newkey rsa:2048 -nodes \
  -keyout host.key -out host.csr \
  -subj &amp;quot;&#x2F;C=DK&#x2F;O=Example ApS&#x2F;CN=example.com&amp;quot; \
  -addext &amp;quot;subjectAltName=DNS:example.com,DNS:www.example.com&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-addext subjectAltName=…&lt;&#x2F;code&gt; is the part people forget — without SANs the request is useless to a modern CA.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-do-i-read-a-csr-that-s-in-der-binary-not-pem&quot;&gt;How do I read a CSR that’s in DER (binary), not PEM?&lt;&#x2F;h3&gt;
&lt;p&gt;Add &lt;code&gt;-inform der&lt;&#x2F;code&gt;: &lt;code&gt;openssl req -in host.der -inform der -noout -text&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-paste-a-csr-online-to-decode-it&quot;&gt;Can I paste a CSR online to decode it?&lt;&#x2F;h3&gt;
&lt;p&gt;You can, but don’t get in the habit — a CSR contains your public key and identity, and the paste site sees all of it. Decoding locally with OpenSSL leaks nothing.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-s-the-difference-between-a-csr-and-a-certificate&quot;&gt;What’s the difference between a CSR and a certificate?&lt;&#x2F;h3&gt;
&lt;p&gt;The CSR is the &lt;em&gt;request&lt;&#x2F;em&gt; (your public key + identity, self-signed). The certificate is the CA’s &lt;em&gt;signed answer&lt;&#x2F;em&gt;. Read a certificate with &lt;code&gt;openssl x509 -in host.crt -noout -text&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Read: &lt;code&gt;openssl req -in host.csr -noout -text -verify&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Check the &lt;strong&gt;SANs&lt;&#x2F;strong&gt;, not just the CN — that’s what browsers trust.&lt;&#x2F;li&gt;
&lt;li&gt;Confirm key&#x2F;CSR&#x2F;cert belong together by comparing &lt;code&gt;-modulus | openssl md5&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Inspect and verify a TLS certificate with OpenSSL (2026)</title>
        <published>2009-05-06T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/inspect-verify-tls-certificate-openssl/"/>
        <id>https://www.x2q.net/post/inspect-verify-tls-certificate-openssl/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/inspect-verify-tls-certificate-openssl/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;openssl x509 -in cert.crt -noout -text&lt;&#x2F;code&gt; dumps a certificate in readable form. Compare &lt;code&gt;-modulus | openssl md5&lt;&#x2F;code&gt; of the cert and the key to confirm they match. Use &lt;code&gt;openssl s_client -connect host:443&lt;&#x2F;code&gt; to see what a live server serves.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Two short 2009 notes (“view x509 details” and “verify a cert matches a key”), merged and refreshed into one. Companion to &lt;a href=&quot;&#x2F;post&#x2F;view-csr-contents-openssl&#x2F;&quot;&gt;reading a CSR&lt;&#x2F;a&gt; and &lt;a href=&quot;&#x2F;post&#x2F;openssl-remove-passphrase-private-key&#x2F;&quot;&gt;removing a key passphrase&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;view-a-certificate&quot;&gt;View a certificate&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -in filename.crt -noout -text
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;filename&lt;&#x2F;code&gt; is the X.509 file — typically &lt;code&gt;.crt&lt;&#x2F;code&gt;, &lt;code&gt;.cert&lt;&#x2F;code&gt;, &lt;code&gt;.pem&lt;&#x2F;code&gt;. &lt;code&gt;-noout&lt;&#x2F;code&gt; skips re-printing the encoded blob; &lt;code&gt;-text&lt;&#x2F;code&gt; gives the human-readable view: subject, issuer, validity dates, key, and extensions.&lt;&#x2F;p&gt;
&lt;p&gt;Pull out just the fields you care about:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -in cert.crt -noout -subject -issuer -dates
openssl x509 -in cert.crt -noout -ext subjectAltName     # the SANs
openssl x509 -in cert.crt -noout -fingerprint -sha256
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;strong&gt;SANs&lt;&#x2F;strong&gt; are the part that matters most now — browsers ignore the CN and only trust the Subject Alternative Names, so confirm every hostname is listed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verify-a-certificate-matches-a-private-key&quot;&gt;Verify a certificate matches a private key&lt;&#x2F;h2&gt;
&lt;p&gt;The classic cutover failure: the cert and key don’t belong together, and TLS refuses to start. The certificate, key (and CSR) share the same public modulus — compare hashes:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;(openssl x509 -noout -modulus -in server.crt | openssl md5; \
 openssl rsa  -noout -modulus -in server.key | openssl md5) | uniq
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;One line of output → they match. Two lines → they don’t&lt;&#x2F;strong&gt; (&lt;code&gt;uniq&lt;&#x2F;code&gt; collapses identical hashes). For an EC key, compare the public keys instead:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;diff &amp;lt;(openssl x509 -in server.crt -noout -pubkey) \
     &amp;lt;(openssl ec   -in server.key -pubout)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No diff → they match.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;inspect-what-a-live-server-is-serving&quot;&gt;Inspect what a live server is serving&lt;&#x2F;h2&gt;
&lt;p&gt;To see the certificate an HTTPS host actually presents (expiry, chain, SANs):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client -connect example.com:443 -servername example.com &amp;lt;&#x2F;dev&#x2F;null 2&amp;gt;&#x2F;dev&#x2F;null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-servername&lt;&#x2F;code&gt; sends SNI, so you get the right cert on a multi-site host. Quick expiry check:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;echo | openssl s_client -connect example.com:443 -servername example.com 2&amp;gt;&#x2F;dev&#x2F;null \
  | openssl x509 -noout -enddate
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;other-formats&quot;&gt;Other formats&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;openssl x509 -in cert.der -inform der -noout -text   # DER (binary)
openssl pkcs12 -in cert.pfx -info -nodes              # inspect a .pfx&#x2F;.p12 bundle
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;no-certificate-matches-private-key-what-now&quot;&gt;“No certificate matches private key” — what now?&lt;&#x2F;h3&gt;
&lt;p&gt;The cert and key are from different keypairs. Run the modulus check above; if the hashes differ, find the key that was used to generate this cert’s CSR (or reissue the cert from the key you have).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-check-the-full-chain&quot;&gt;How do I check the full chain?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;openssl verify -untrusted intermediate.crt server.crt&lt;&#x2F;code&gt;, or inspect what &lt;code&gt;s_client&lt;&#x2F;code&gt; returns with &lt;code&gt;-showcerts&lt;&#x2F;code&gt; to see every cert the server sends.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;when-does-this-cert-expire-across-many-hosts&quot;&gt;When does this cert expire, across many hosts?&lt;&#x2F;h3&gt;
&lt;p&gt;Script the &lt;code&gt;s_client … -enddate&lt;&#x2F;code&gt; one-liner over a host list — it’s the no-dependencies way to monitor expiry.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;View: &lt;code&gt;openssl x509 -in cert.crt -noout -text&lt;&#x2F;code&gt; (and &lt;code&gt;-subject -dates -ext subjectAltName&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;Match cert↔key: compare &lt;code&gt;-modulus | openssl md5&lt;&#x2F;code&gt; — one line = match.&lt;&#x2F;li&gt;
&lt;li&gt;Live server: &lt;code&gt;openssl s_client -connect host:443 -servername host&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Recover RAR, 7z, and ZIP archive passwords on Linux (2026)</title>
        <published>2009-03-22T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/crack-rar-7z-zip-passwords-linux/"/>
        <id>https://www.x2q.net/post/crack-rar-7z-zip-passwords-linux/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/crack-rar-7z-zip-passwords-linux/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; extract a hash with &lt;code&gt;rar2john&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;7z2john&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;zip2john&lt;&#x2F;code&gt;, then crack it with &lt;code&gt;hashcat&lt;&#x2F;code&gt; (GPU) or &lt;code&gt;john&lt;&#x2F;code&gt;. Dictionary attack first, then a targeted mask. This replaces the old &lt;code&gt;rarcrack&lt;&#x2F;code&gt; tool, which is unmaintained and CPU-only.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 2009 version of this post used &lt;strong&gt;rarcrack&lt;&#x2F;strong&gt; — abandoned for over a decade. The modern, far faster approach is John the Ripper’s hash extractors + hashcat on a GPU. For the ZIP-specific deep dive, see the &lt;a href=&quot;&#x2F;post&#x2F;crack-password-encrypted-zip&#x2F;&quot;&gt;fcrackzip post&lt;&#x2F;a&gt;; this one covers RAR and 7z too. Only do this on archives you own or are authorised to access.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-two-step-approach&quot;&gt;The two-step approach&lt;&#x2F;h2&gt;
&lt;p&gt;Modern crackers don’t attack the archive directly. You &lt;strong&gt;extract a hash&lt;&#x2F;strong&gt; that represents the password check, then throw a cracker at the hash. The extractors ship with John the Ripper:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install john          # provides rar2john, zip2john, and the *2john scripts
sudo apt install hashcat        # GPU-accelerated cracker
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;rar&quot;&gt;RAR&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;rar2john archive.rar &amp;gt; hash.txt
cat hash.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The hash format tells you the RAR version. Pick the hashcat mode:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# RAR3 (-hp, older):
hashcat -m 12500 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt

# RAR5 (modern):
hashcat -m 13000 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;7-zip&quot;&gt;7-Zip&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;7z2john&lt;&#x2F;code&gt; is a Perl script (install &lt;code&gt;libcompress-raw-lzma-perl&lt;&#x2F;code&gt; if it complains):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;7z2john archive.7z &amp;gt; hash.txt        # or: 7z2john.pl archive.7z &amp;gt; hash.txt
hashcat -m 11600 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;7-Zip uses AES-256 with many KDF iterations, so it’s &lt;strong&gt;slow&lt;&#x2F;strong&gt; to crack — a good wordlist matters far more than raw speed here.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zip&quot;&gt;ZIP&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;zip2john archive.zip &amp;gt; hash.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which mode depends on the encryption:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# Classic ZipCrypto (weak, old PKZIP):
hashcat -m 17200 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt

# WinZip AES (modern, strong):
hashcat -m 13600 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For ZipCrypto specifically, the lightweight &lt;code&gt;fcrackzip&lt;&#x2F;code&gt; is also an option — &lt;a href=&quot;&#x2F;post&#x2F;crack-password-encrypted-zip&#x2F;&quot;&gt;covered separately&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dictionary-first-then-masks&quot;&gt;Dictionary first, then masks&lt;&#x2F;h2&gt;
&lt;p&gt;Most recoveries succeed with a wordlist — start there (&lt;code&gt;rockyou.txt&lt;&#x2F;code&gt; ships with Kali, or grab it anywhere). If you remember the &lt;em&gt;shape&lt;&#x2F;em&gt; of the password, a &lt;strong&gt;mask attack&lt;&#x2F;strong&gt; is far more efficient than blind brute force:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# 8 chars: capital + 5 lowercase + 2 digits, e.g. &amp;quot;Summer26&amp;quot;
hashcat -m 13000 -a 3 hash.txt &amp;#39;?u?l?l?l?l?l?d?d&amp;#39;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Add rules to a dictionary run to cover common mutations:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;hashcat -m 13000 hash.txt rockyou.txt -r &#x2F;usr&#x2F;share&#x2F;hashcat&#x2F;rules&#x2F;best64.rule
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;what-to-expect&quot;&gt;What to expect&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU vs CPU:&lt;&#x2F;strong&gt; hashcat on a modern GPU is orders of magnitude faster than the old CPU tools. ZipCrypto cracks at billions&#x2F;sec; RAR5 and 7z are deliberately slow (strong KDF), so lean on good wordlists and masks rather than exhaustive brute force.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Resume:&lt;&#x2F;strong&gt; hashcat checkpoints automatically — &lt;code&gt;--restore&lt;&#x2F;code&gt; continues an interrupted run.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Find the running guess:&lt;&#x2F;strong&gt; press &lt;code&gt;s&lt;&#x2F;code&gt; for status during a run.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;hashcat-says-no-hashes-loaded-or-token-length-exception&quot;&gt;hashcat says “No hashes loaded” or “Token length exception”&lt;&#x2F;h3&gt;
&lt;p&gt;The extracted hash includes a prefix the mode must match (e.g. RAR3 vs RAR5). Check the start of &lt;code&gt;hash.txt&lt;&#x2F;code&gt; against the mode you chose; &lt;code&gt;hashcat --identify hash.txt&lt;&#x2F;code&gt; can help.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-this-legal&quot;&gt;Is this legal?&lt;&#x2F;h3&gt;
&lt;p&gt;Recovering a password on an archive you own or are authorised to access is fine. Doing it to someone else’s file without permission is unauthorised access — the usual computer-misuse laws apply.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;strong-password-dictionary-failed-now-what&quot;&gt;Strong password, dictionary failed — now what?&lt;&#x2F;h3&gt;
&lt;p&gt;If it’s a long random password on RAR5&#x2F;7z&#x2F;WinZip-AES, it may simply be out of reach — that’s the encryption working as intended. Spend your effort on a smarter wordlist&#x2F;mask built from what you remember, not on brute-forcing length.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Extract: &lt;code&gt;rar2john&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;7z2john&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;zip2john&lt;&#x2F;code&gt; → &lt;code&gt;hash.txt&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Crack: hashcat modes — RAR3 &lt;code&gt;12500&lt;&#x2F;code&gt;, RAR5 &lt;code&gt;13000&lt;&#x2F;code&gt;, 7z &lt;code&gt;11600&lt;&#x2F;code&gt;, ZipCrypto &lt;code&gt;17200&lt;&#x2F;code&gt;, WinZip-AES &lt;code&gt;13600&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Wordlist + rules first, then targeted masks. rarcrack is obsolete — don’t bother.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Compress a mysqldump backup — gzip, zstd, xz (2026)</title>
        <published>2009-01-31T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/compress-mysqldump-output/"/>
        <id>https://www.x2q.net/post/compress-mysqldump-output/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/compress-mysqldump-output/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;mysqldump db | zstd &amp;gt; db.sql.zst&lt;&#x2F;code&gt; dumps and compresses in one pipe — no giant intermediate file. Restore with &lt;code&gt;zstd -dc db.sql.zst | mysql db&lt;&#x2F;code&gt;. Use &lt;code&gt;gzip&lt;&#x2F;code&gt; for maximum portability, &lt;code&gt;zstd&lt;&#x2F;code&gt; for the best speed&#x2F;size balance, &lt;code&gt;xz&lt;&#x2F;code&gt; for the smallest file.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2009 note — the pipe pattern is timeless, but the original reached for &lt;code&gt;lzma&lt;&#x2F;code&gt;. In 2026 &lt;strong&gt;zstd&lt;&#x2F;strong&gt; is the one to use; the commands for all the common compressors are below.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;dump-and-compress-in-one-pipe&quot;&gt;Dump and compress in one pipe&lt;&#x2F;h2&gt;
&lt;p&gt;Piping avoids ever writing the full uncompressed dump to disk:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# zstd — modern default: fast, great ratio
mysqldump mydb | zstd -o mydb.sql.zst

# gzip — universal, on every system
mysqldump mydb | gzip &amp;gt; mydb.sql.gz

# xz — smallest file, slowest
mysqldump mydb | xz &amp;gt; mydb.sql.xz
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Add the options you’d normally pass to &lt;code&gt;mysqldump&lt;&#x2F;code&gt; (credentials, flags) before the database name.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;restore-decompress-back-into-mysql&quot;&gt;Restore: decompress back into mysql&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;zstd -dc mydb.sql.zst | mysql mydb
gunzip  &amp;lt; mydb.sql.gz  | mysql mydb
xz -dc   mydb.sql.xz   | mysql mydb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-dc&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;gunzip &amp;lt;&lt;&#x2F;code&gt; stream straight to &lt;code&gt;mysql&lt;&#x2F;code&gt; — again, no temp file.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-sane-production-dump-line&quot;&gt;A sane production dump line&lt;&#x2F;h2&gt;
&lt;p&gt;For a consistent, restorable backup of an InnoDB database without locking it:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump --single-transaction --quick --routines --triggers --events \
  mydb | zstd &amp;gt; mydb-$(date +%F).sql.zst
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--single-transaction&lt;&#x2F;code&gt; — consistent snapshot without locking (InnoDB).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--quick&lt;&#x2F;code&gt; — stream rows instead of buffering (low memory on huge tables).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--routines --triggers --events&lt;&#x2F;code&gt; — include stored programs, not just data.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mariadb&quot;&gt;MariaDB&lt;&#x2F;h2&gt;
&lt;p&gt;Use the &lt;code&gt;mariadb-dump&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;mariadb&lt;&#x2F;code&gt; client names (the &lt;code&gt;mysqldump&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;mysql&lt;&#x2F;code&gt; names still exist as compatibility symlinks):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;mariadb-dump --single-transaction mydb | zstd &amp;gt; mydb.sql.zst
zstd -dc mydb.sql.zst | mariadb mydb
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;speed-vs-size-roughly&quot;&gt;Speed vs size, roughly&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gzip&lt;&#x2F;strong&gt; — fast, ~everywhere, moderate ratio. Safe default if you can’t install anything.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;zstd&lt;&#x2F;strong&gt; — as fast as gzip (often faster), notably smaller; &lt;code&gt;zstd -19&lt;&#x2F;code&gt; or &lt;code&gt;--long&lt;&#x2F;code&gt; for archival. Best all-rounder.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;xz&lt;&#x2F;strong&gt; — smallest output, much slower CPU. Good for cold archives where size is king.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Tune threads on big dumps: &lt;code&gt;zstd -T0&lt;&#x2F;code&gt; (all cores), &lt;code&gt;xz -T0&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-do-i-check-the-dump-without-restoring-it&quot;&gt;How do I check the dump without restoring it?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;zstd -dc mydb.sql.zst | head -50&lt;&#x2F;code&gt; — peek at the SQL header. &lt;code&gt;zstd -t&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;gzip -t&lt;&#x2F;code&gt; test archive integrity.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-dump-just-one-table&quot;&gt;Can I dump just one table?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;mysqldump mydb mytable | zstd &amp;gt; mytable.sql.zst&lt;&#x2F;code&gt;. Add more table names to include several.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;restore-is-slow-anything-to-do&quot;&gt;Restore is slow — anything to do?&lt;&#x2F;h3&gt;
&lt;p&gt;Wrap large imports with &lt;code&gt;SET autocommit=0; … COMMIT;&lt;&#x2F;code&gt; and disable keys during load, or restore from a physical backup tool (Percona XtraBackup &#x2F; mariabackup) for very large datasets.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Dump+compress: &lt;code&gt;mysqldump db | zstd &amp;gt; db.sql.zst&lt;&#x2F;code&gt; (or &lt;code&gt;gzip&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;xz&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;Restore: &lt;code&gt;zstd -dc db.sql.zst | mysql db&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Production: add &lt;code&gt;--single-transaction --quick --routines --triggers&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;zstd is the modern pick; gzip for portability; xz for smallest.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Remove the password &#x2F; editing protection from a Word document (2026)</title>
        <published>2008-07-28T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/remove-word-document-password/"/>
        <id>https://www.x2q.net/post/remove-word-document-password/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/remove-word-document-password/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; if a Word doc is &lt;strong&gt;read-only &#x2F; restricted from editing&lt;&#x2F;strong&gt;, a &lt;code&gt;.docx&lt;&#x2F;code&gt; is just a ZIP — unzip it, delete the &lt;code&gt;&amp;lt;w:documentProtection&amp;gt;&lt;&#x2F;code&gt; element from &lt;code&gt;word&#x2F;settings.xml&lt;&#x2F;code&gt;, re-zip. If it’s &lt;strong&gt;password-to-open&lt;&#x2F;strong&gt; (encrypted), that’s real crypto: extract a hash with &lt;code&gt;office2john&lt;&#x2F;code&gt; and recover it with &lt;code&gt;hashcat&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 2008 version of this used Office XP’s “Script Editor” — long gone. The modern &lt;code&gt;.docx&lt;&#x2F;code&gt; format makes editing-protection removal even easier (it’s a ZIP), and the distinction between &lt;em&gt;editing protection&lt;&#x2F;em&gt; and &lt;em&gt;real encryption&lt;&#x2F;em&gt; is the thing to get right. Only do this on documents you own or are authorised to modify.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;first-which-kind-of-password-is-it&quot;&gt;First: which kind of “password” is it?&lt;&#x2F;h2&gt;
&lt;p&gt;Two completely different protections get called “password” in Word:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Editing &#x2F; read-only protection (“Restrict Editing”).&lt;&#x2F;strong&gt; The document opens fine; you just can’t edit it. This is &lt;strong&gt;not encryption&lt;&#x2F;strong&gt; — it’s a flag in the file. Trivial to remove.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Password to open (“Encrypt with Password”).&lt;&#x2F;strong&gt; The document won’t open at all without the password. This &lt;strong&gt;is&lt;&#x2F;strong&gt; strong AES encryption. You can’t “remove” it — you have to recover the password.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;case-1-remove-editing-read-only-protection&quot;&gt;Case 1 — remove editing &#x2F; read-only protection&lt;&#x2F;h2&gt;
&lt;p&gt;A modern &lt;code&gt;.docx&lt;&#x2F;code&gt; is a ZIP archive of XML. The protection lives in one element.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# work on a copy
cp protected.docx unlocked.docx
mkdir extracted &amp;amp;&amp;amp; cd extracted
unzip ..&#x2F;unlocked.docx
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Edit &lt;code&gt;word&#x2F;settings.xml&lt;&#x2F;code&gt; and delete the whole &lt;code&gt;&amp;lt;w:documentProtection ...&#x2F;&amp;gt;&lt;&#x2F;code&gt; element. It looks like:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;w:documentProtection w:edit=&amp;quot;readOnly&amp;quot; w:enforcement=&amp;quot;1&amp;quot;
  w:cryptProviderType=&amp;quot;rsaAES&amp;quot; w:cryptAlgorithmSid=&amp;quot;14&amp;quot;
  w:hash=&amp;quot;…&amp;quot; w:salt=&amp;quot;…&amp;quot;&#x2F;&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Remove that one self-closing tag, then repackage:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;zip -r ..&#x2F;unlocked.docx .        # re-zip the contents
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Open &lt;code&gt;unlocked.docx&lt;&#x2F;code&gt; — fully editable. (The &lt;code&gt;w:hash&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;w:salt&lt;&#x2F;code&gt; only protect &lt;em&gt;re-enabling&lt;&#x2F;em&gt; the restriction in the UI; deleting the element ignores them entirely, which is why this “protection” is cosmetic.)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;even-simpler-libreoffice&quot;&gt;Even simpler: LibreOffice&lt;&#x2F;h3&gt;
&lt;p&gt;Open the file in &lt;strong&gt;LibreOffice Writer → Edit → Edit Mode&lt;&#x2F;strong&gt; (or &lt;strong&gt;Format → Sections&lt;&#x2F;strong&gt; &#x2F; &lt;strong&gt;Tools → Protect Document&lt;&#x2F;strong&gt; depending on the protection), turn the protection off, and save. For form&#x2F;section protection, LibreOffice toggles it without any password.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;case-2-password-to-open-encrypted&quot;&gt;Case 2 — password to open (encrypted)&lt;&#x2F;h2&gt;
&lt;p&gt;If Word demands a password just to &lt;em&gt;open&lt;&#x2F;em&gt; the file, the content is AES-encrypted and there’s no shortcut — you recover the password with the same hash-then-crack flow as &lt;a href=&quot;&#x2F;post&#x2F;crack-rar-7z-zip-passwords-linux&#x2F;&quot;&gt;archive passwords&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# office2john ships with John the Ripper
office2john protected.docx &amp;gt; hash.txt

# hashcat mode depends on the Office version that saved it:
#   9400 = Office 2007, 9500 = 2010, 9600 = 2013+ (most modern files)
hashcat -m 9600 hash.txt &#x2F;usr&#x2F;share&#x2F;wordlists&#x2F;rockyou.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Modern Office encryption uses a heavy KDF, so it’s &lt;strong&gt;slow&lt;&#x2F;strong&gt; — a good, targeted wordlist (and a mask built from what you remember) beats brute force. If it was a long random password, it may be genuinely unrecoverable, which is the encryption doing its job.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;old-doc-binary-format&quot;&gt;Old .doc (binary format)&lt;&#x2F;h2&gt;
&lt;p&gt;The pre-2007 &lt;code&gt;.doc&lt;&#x2F;code&gt; binary format isn’t a ZIP. For &lt;em&gt;editing&lt;&#x2F;em&gt; protection, opening and re-saving in LibreOffice usually drops it; for password-to-open, &lt;code&gt;office2john&lt;&#x2F;code&gt; handles old &lt;code&gt;.doc&lt;&#x2F;code&gt; too (it autodetects the format) — same crack flow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-read-only-protection-actually-security&quot;&gt;Is read-only protection actually security?&lt;&#x2F;h3&gt;
&lt;p&gt;No. As above, it’s a removable flag, not encryption — useful to prevent &lt;em&gt;accidental&lt;&#x2F;em&gt; edits, useless against anyone who doesn’t want to be stopped. If you need real protection, use &lt;strong&gt;Encrypt with Password&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;excel-powerpoint&quot;&gt;Excel &#x2F; PowerPoint?&lt;&#x2F;h3&gt;
&lt;p&gt;Same model. &lt;code&gt;.xlsx&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.pptx&lt;&#x2F;code&gt; are ZIPs — sheet&#x2F;workbook protection is an element you can delete; password-to-open is encryption, recoverable via &lt;code&gt;office2john&lt;&#x2F;code&gt; (mode 9600 etc.).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;is-this-legal&quot;&gt;Is this legal?&lt;&#x2F;h3&gt;
&lt;p&gt;On your own documents, or ones you’re authorised to edit — yes. Removing protection from someone else’s confidential document without permission is not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Editing&#x2F;read-only&lt;&#x2F;strong&gt;: unzip the &lt;code&gt;.docx&lt;&#x2F;code&gt;, delete &lt;code&gt;&amp;lt;w:documentProtection&amp;gt;&lt;&#x2F;code&gt; from &lt;code&gt;word&#x2F;settings.xml&lt;&#x2F;code&gt;, re-zip — or just toggle it off in LibreOffice.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Password-to-open&lt;&#x2F;strong&gt;: real encryption — &lt;code&gt;office2john file.docx &amp;gt; hash.txt&lt;&#x2F;code&gt;, then &lt;code&gt;hashcat -m 9600 …&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Read-only protection is not security; use real encryption if you need it.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Convert Windows line endings to Unix with dos2unix (2026)</title>
        <published>2008-04-30T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/dos2unix-convert-line-endings/"/>
        <id>https://www.x2q.net/post/dos2unix-convert-line-endings/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/dos2unix-convert-line-endings/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;dos2unix file.txt&lt;&#x2F;code&gt; rewrites a file’s CRLF (Windows) line endings to LF (Unix) in place. Install it with &lt;code&gt;sudo apt install dos2unix&lt;&#x2F;code&gt;. No dos2unix? &lt;code&gt;sed -i &#x27;s&#x2F;\r$&#x2F;&#x2F;&#x27; file.txt&lt;&#x2F;code&gt; does the same.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2008 note that pointed at the &lt;code&gt;tofrodos&lt;&#x2F;code&gt; package. On modern Debian&#x2F;Ubuntu &lt;strong&gt;&lt;code&gt;dos2unix&lt;&#x2F;code&gt; is its own package now&lt;&#x2F;strong&gt; — simpler. The rest still applies: CRLF vs LF mismatches are a perennial nuisance.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;why-it-matters&quot;&gt;Why it matters&lt;&#x2F;h2&gt;
&lt;p&gt;DOS&#x2F;Windows ends each line with &lt;strong&gt;CR+LF&lt;&#x2F;strong&gt; (&lt;code&gt;\r\n&lt;&#x2F;code&gt;); Unix&#x2F;Linux&#x2F;macOS use just &lt;strong&gt;LF&lt;&#x2F;strong&gt; (&lt;code&gt;\n&lt;&#x2F;code&gt;). A file authored on Windows and run on Linux carries stray &lt;code&gt;\r&lt;&#x2F;code&gt; characters that cause:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#!&#x2F;bin&#x2F;bash^M: bad interpreter&lt;&#x2F;code&gt; when running a script.&lt;&#x2F;li&gt;
&lt;li&gt;Configs and &lt;code&gt;.env&lt;&#x2F;code&gt; files where a value silently gains a trailing &lt;code&gt;\r&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Diffs and &lt;code&gt;grep&lt;&#x2F;code&gt; matching things that look identical but aren’t.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install dos2unix        # Debian &#x2F; Ubuntu (its own package now)
sudo dnf install dos2unix         # Fedora
brew install dos2unix             # macOS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;convert&quot;&gt;Convert&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;dos2unix script.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It converts &lt;strong&gt;in place&lt;&#x2F;strong&gt;. Keep a copy under a new name if you want the original:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;dos2unix -n script.sh script.unix.sh
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The reverse exists too — &lt;code&gt;unix2dos file.txt&lt;&#x2F;code&gt; adds CRLF back (e.g. for a file a Windows tool insists on).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;batch-a-whole-tree&quot;&gt;Batch a whole tree&lt;&#x2F;h2&gt;
&lt;p&gt;Convert every shell script under the current directory:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &amp;#39;*.sh&amp;#39; -exec dos2unix {} +
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;dos2unix&lt;&#x2F;code&gt; skips binary files it detects, so a broad &lt;code&gt;find . -type f&lt;&#x2F;code&gt; is usually safe — but scope it with &lt;code&gt;-name&lt;&#x2F;code&gt; when you can.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;no-dos2unix-installed-use-sed&quot;&gt;No dos2unix installed? Use sed&lt;&#x2F;h2&gt;
&lt;p&gt;Strip the trailing CR with &lt;code&gt;sed&lt;&#x2F;code&gt; (in place with &lt;code&gt;-i&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sed -i &amp;#39;s&#x2F;\r$&#x2F;&#x2F;&amp;#39; file.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Or with &lt;code&gt;tr&lt;&#x2F;code&gt; (writes to a new file — &lt;code&gt;tr&lt;&#x2F;code&gt; can’t edit in place):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;tr -d &amp;#39;\r&amp;#39; &amp;lt; dosfile.txt &amp;gt; unixfile.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;check-what-you-ve-got&quot;&gt;Check what you’ve got&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;file script.sh
script.sh: Bourne-Again shell script, ASCII text, with CRLF line terminators
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;“with CRLF line terminators” is the tell. &lt;code&gt;cat -A file&lt;&#x2F;code&gt; shows &lt;code&gt;^M&lt;&#x2F;code&gt; at each line end.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;macos-sed-says-invalid-command-code&quot;&gt;macOS sed says “invalid command code”&lt;&#x2F;h3&gt;
&lt;p&gt;BSD &lt;code&gt;sed&lt;&#x2F;code&gt; (macOS) needs an argument after &lt;code&gt;-i&lt;&#x2F;code&gt;: &lt;code&gt;sed -i &#x27;&#x27; &#x27;s&#x2F;\r$&#x2F;&#x2F;&#x27; file.txt&lt;&#x2F;code&gt;. Or just &lt;code&gt;brew install dos2unix&lt;&#x2F;code&gt; and avoid the difference.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;git-keeps-reintroducing-crlf&quot;&gt;Git keeps reintroducing CRLF&lt;&#x2F;h3&gt;
&lt;p&gt;That’s Git’s &lt;code&gt;core.autocrlf&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;.gitattributes&lt;&#x2F;code&gt;, not the file itself. Set &lt;code&gt;* text=auto eol=lf&lt;&#x2F;code&gt; in &lt;code&gt;.gitattributes&lt;&#x2F;code&gt; to normalise on commit.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-this-change-the-file-s-encoding-too&quot;&gt;Does this change the file’s encoding too?&lt;&#x2F;h3&gt;
&lt;p&gt;No — line endings and character encoding are independent. For UTF-8&#x2F;Latin-1 conversion see &lt;a href=&quot;&#x2F;post&#x2F;convert-file-encoding-iconv&#x2F;&quot;&gt;iconv&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dos2unix file&lt;&#x2F;code&gt; — CRLF → LF, in place. &lt;code&gt;unix2dos&lt;&#x2F;code&gt; for the reverse.&lt;&#x2F;li&gt;
&lt;li&gt;Install: &lt;code&gt;sudo apt install dos2unix&lt;&#x2F;code&gt; (now its own package).&lt;&#x2F;li&gt;
&lt;li&gt;Batch: &lt;code&gt;find . -name &#x27;*.sh&#x27; -exec dos2unix {} +&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Fallback: &lt;code&gt;sed -i &#x27;s&#x2F;\r$&#x2F;&#x2F;&#x27; file&lt;&#x2F;code&gt; (&lt;code&gt;sed -i &#x27;&#x27; …&lt;&#x2F;code&gt; on macOS).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Mount a remote filesystem over SSH with sshfs (2026)</title>
        <published>2008-03-06T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/mount-remote-filesystem-sshfs/"/>
        <id>https://www.x2q.net/post/mount-remote-filesystem-sshfs/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/mount-remote-filesystem-sshfs/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;sshfs user@host:&#x2F;remote&#x2F;path &#x2F;mnt&#x2F;point -o reconnect,uid=$(id -u),gid=$(id -g)&lt;&#x2F;code&gt; mounts a remote directory locally over SSH. Unmount with &lt;code&gt;fusermount -u &#x2F;mnt&#x2F;point&lt;&#x2F;code&gt;. The server needs nothing beyond OpenSSH, which it already has.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2008 note that still holds up — if anything sshfs is more useful now that everything’s a remote box. Pairs nicely with &lt;a href=&quot;&#x2F;post&#x2F;ssh-key-login-without-password&#x2F;&quot;&gt;SSH key login&lt;&#x2F;a&gt; so the mount comes up without a password prompt.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;why-sshfs&quot;&gt;Why sshfs&lt;&#x2F;h2&gt;
&lt;p&gt;It’s a FUSE filesystem that speaks the SSH &lt;strong&gt;SFTP&lt;&#x2F;strong&gt; protocol. Because every server running OpenSSH already supports SFTP, there is &lt;strong&gt;nothing to set up on the server side&lt;&#x2F;strong&gt; — if you can &lt;code&gt;ssh&lt;&#x2F;code&gt; in, you can &lt;code&gt;sshfs&lt;&#x2F;code&gt; in. Great for editing remote files with local tools, copying across, or poking at a server’s filesystem without a full sync.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install sshfs        # Debian &#x2F; Ubuntu
sudo dnf install fuse-sshfs    # Fedora
brew install macfuse           # macOS: needs macFUSE + the sshfs cask
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;mount&quot;&gt;Mount&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~&#x2F;mnt&#x2F;server
sshfs user@remote.host:&#x2F;home&#x2F;user ~&#x2F;mnt&#x2F;server -o reconnect,uid=$(id -u),gid=$(id -g)
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;uid&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;gid&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: map remote file ownership to &lt;em&gt;your&lt;&#x2F;em&gt; local user so you have read&#x2F;write access (otherwise files may show up owned by someone else and be read-only).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reconnect&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: transparently re-establish the connection if SSH drops.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now &lt;code&gt;~&#x2F;mnt&#x2F;server&lt;&#x2F;code&gt; is the remote directory — use &lt;code&gt;ls&lt;&#x2F;code&gt;, your editor, file manager, anything.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keep-it-alive-on-flaky-links&quot;&gt;Keep it alive on flaky links&lt;&#x2F;h2&gt;
&lt;p&gt;Network blips otherwise leave the mount hung. Add keepalive + auto-reconnect options:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;sshfs user@host:&#x2F;path ~&#x2F;mnt&#x2F;server \
  -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,follow_symlinks
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;ServerAliveInterval&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;CountMax&lt;&#x2F;code&gt; make SSH notice a dead link in ~45 s; &lt;code&gt;follow_symlinks&lt;&#x2F;code&gt; resolves remote symlinks locally.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;unmount&quot;&gt;Unmount&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;fusermount -u ~&#x2F;mnt&#x2F;server     # Linux
umount ~&#x2F;mnt&#x2F;server            # macOS
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Use &lt;code&gt;fusermount -uz&lt;&#x2F;code&gt; (lazy) if it’s stuck on a dead connection.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mount-automatically-via-fstab&quot;&gt;Mount automatically via fstab&lt;&#x2F;h2&gt;
&lt;p&gt;For a mount you want back after reboot (key-based auth required, since fstab can’t type a password):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;user@host:&#x2F;path  &#x2F;home&#x2F;me&#x2F;mnt&#x2F;server  fuse.sshfs  noauto,x-systemd.automount,_netdev,reconnect,uid=1000,gid=1000,IdentityFile=&#x2F;home&#x2F;me&#x2F;.ssh&#x2F;id_ed25519  0 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;x-systemd.automount&lt;&#x2F;code&gt; mounts it on first access rather than at boot, so a slow&#x2F;absent server doesn’t stall startup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Files owned by the wrong user &#x2F; read-only&lt;&#x2F;strong&gt; → you forgot &lt;code&gt;uid&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;gid&lt;&#x2F;code&gt;. Set them to your local &lt;code&gt;id -u&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;id -g&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;“Transport endpoint is not connected”&lt;&#x2F;strong&gt; → the SSH link died. &lt;code&gt;fusermount -uz&lt;&#x2F;code&gt; then remount; add &lt;code&gt;reconnect&lt;&#x2F;code&gt; to avoid it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Slow with many small files&lt;&#x2F;strong&gt; → SFTP is chatty; add &lt;code&gt;-o cache=yes,kernel_cache,compression=no&lt;&#x2F;code&gt; and avoid running e.g. &lt;code&gt;grep -r&lt;&#x2F;code&gt; over huge remote trees.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;macOS&lt;&#x2F;strong&gt; → macFUSE needs a one-time approval in System Settings → Privacy &amp;amp; Security after install.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;is-sshfs-secure&quot;&gt;Is sshfs secure?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — all traffic rides inside the SSH connection, same encryption as a normal SSH session.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-the-server-admin-need-to-install-anything&quot;&gt;Does the server admin need to install anything?&lt;&#x2F;h3&gt;
&lt;p&gt;No. SFTP ships with OpenSSH. If you can SSH in, sshfs works.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sshfs-vs-nfs-samba&quot;&gt;sshfs vs NFS&#x2F;Samba?&lt;&#x2F;h3&gt;
&lt;p&gt;NFS&#x2F;Samba are faster for LAN file serving but need server-side setup and open ports. sshfs needs only SSH — ideal for ad-hoc access to a remote box you can already log into.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sshfs user@host:&#x2F;path &#x2F;mnt -o reconnect,uid=$(id -u),gid=$(id -g)&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Set &lt;code&gt;uid&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;gid&lt;&#x2F;code&gt; for write access; add &lt;code&gt;ServerAliveInterval&lt;&#x2F;code&gt; + &lt;code&gt;reconnect&lt;&#x2F;code&gt; for stability.&lt;&#x2F;li&gt;
&lt;li&gt;Unmount with &lt;code&gt;fusermount -u&lt;&#x2F;code&gt; (Linux) &#x2F; &lt;code&gt;umount&lt;&#x2F;code&gt; (macOS).&lt;&#x2F;li&gt;
&lt;li&gt;Combine with key auth for a password-free mount.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Find the most recently changed files on Linux (2026)</title>
        <published>2007-10-30T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/find-recently-changed-files-linux/"/>
        <id>https://www.x2q.net/post/find-recently-changed-files-linux/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/find-recently-changed-files-linux/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;find . -type f -printf &#x27;%T+ %p\n&#x27; | sort | tail&lt;&#x2F;code&gt; lists files newest-last across a whole tree. To filter by age, &lt;code&gt;find . -type f -mmin -60&lt;&#x2F;code&gt; (last hour) or &lt;code&gt;-mtime -1&lt;&#x2F;code&gt; (last day). On macOS, use the &lt;code&gt;stat&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-newermt&lt;&#x2F;code&gt; variants below.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A one-line note from 2007 that’s still in my muscle memory. “What changed on this box recently?” comes up constantly — here’s the full toolkit.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;newest-files-in-a-directory-tree-gnu-linux&quot;&gt;Newest files in a directory tree (GNU&#x2F;Linux)&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -printf &amp;#39;%T+ %p\n&amp;#39; | sort | tail -20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-printf &#x27;%T+ %p\n&#x27;&lt;&#x2F;code&gt; prints a sortable &lt;code&gt;YYYY-MM-DD+HH:MM:SS&lt;&#x2F;code&gt; timestamp then the path.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sort&lt;&#x2F;code&gt; orders oldest→newest; &lt;code&gt;tail -20&lt;&#x2F;code&gt; shows the 20 most recent (&lt;code&gt;| sort -r | head&lt;&#x2F;code&gt; flips it).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;filter-by-age-directly&quot;&gt;Filter by age directly&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;find&lt;&#x2F;code&gt; can select by modification time without sorting:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -mmin -60      # modified in the last 60 minutes
find . -type f -mmin -5       # last 5 minutes
find . -type f -mtime -1      # last 24 hours (-mtime is in days)
find . -type f -mtime -7      # last week
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The leading &lt;code&gt;-&lt;&#x2F;code&gt; means “less than N ago”; &lt;code&gt;+N&lt;&#x2F;code&gt; means “more than N ago”.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;files-newer-than-a-reference&quot;&gt;Files newer than a reference&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -newer &#x2F;etc&#x2F;some.reference     # changed after that file
find . -type f -newermt &amp;quot;2026-06-01&amp;quot;          # changed after a date (GNU)
find . -type f -newermt &amp;quot;2 hours ago&amp;quot;         # changed in the last 2 hours
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;add-details-size-time-to-the-listing&quot;&gt;Add details (size, time) to the listing&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -mmin -60 -printf &amp;#39;%TY-%Tm-%Td %TT  %10s  %p\n&amp;#39; | sort
# or hand off to ls for human sizes:
find . -type f -mmin -60 -exec ls -lh --time-style=long-iso {} +
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;macos-bsd-find-no-printf&quot;&gt;macOS &#x2F; BSD find (no -printf)&lt;&#x2F;h2&gt;
&lt;p&gt;BSD &lt;code&gt;find&lt;&#x2F;code&gt; on macOS doesn’t support &lt;code&gt;-printf&lt;&#x2F;code&gt;. Use &lt;code&gt;-newermt&lt;&#x2F;code&gt; (it does have that) or fall back to &lt;code&gt;stat&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -mtime -1                       # works on macOS too
find . -type f -newermt &amp;quot;2026-06-01&amp;quot;           # date filter
# newest-last with stat (macOS stat syntax):
find . -type f -exec stat -f &amp;#39;%m %N&amp;#39; {} + | sort -n | tail -20
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(&lt;code&gt;%m&lt;&#x2F;code&gt; = epoch mtime, &lt;code&gt;%N&lt;&#x2F;code&gt; = name; &lt;code&gt;sort -n&lt;&#x2F;code&gt; orders numerically.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;what-s-the-difference-between-modified-changed-and-accessed&quot;&gt;What’s the difference between modified, changed, and accessed?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;-mtime&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;%T&lt;&#x2F;code&gt; = &lt;strong&gt;content&lt;&#x2F;strong&gt; last modified. &lt;code&gt;-ctime&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;%C&lt;&#x2F;code&gt; = inode &lt;strong&gt;changed&lt;&#x2F;strong&gt; (permissions, rename, content). &lt;code&gt;-atime&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;%A&lt;&#x2F;code&gt; = last &lt;strong&gt;accessed&lt;&#x2F;strong&gt; (often disabled via &lt;code&gt;noatime&lt;&#x2F;code&gt; mounts). For “what file changed” you almost always want &lt;code&gt;-mtime&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-mmin&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-find-the-single-newest-file&quot;&gt;How do I find the single newest file?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;find . -type f -printf &#x27;%T@ %p\n&#x27; | sort -n | tail -1 | cut -d&#x27; &#x27; -f2-&lt;&#x2F;code&gt; (&lt;code&gt;%T@&lt;&#x2F;code&gt; is epoch seconds).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;exclude-a-directory-e-g-git&quot;&gt;Exclude a directory (e.g. .git)?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;find . -path .&#x2F;.git -prune -o -type f -mmin -60 -print&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Newest-last list: &lt;code&gt;find . -type f -printf &#x27;%T+ %p\n&#x27; | sort | tail&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;By age: &lt;code&gt;-mmin -60&lt;&#x2F;code&gt; (minutes), &lt;code&gt;-mtime -1&lt;&#x2F;code&gt; (days), &lt;code&gt;-newermt &quot;…&quot;&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;macOS: use &lt;code&gt;-mtime&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-newermt&lt;&#x2F;code&gt;, or &lt;code&gt;stat -f &#x27;%m %N&#x27;&lt;&#x2F;code&gt; since BSD find has no &lt;code&gt;-printf&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>MySQL: find and remove duplicate rows (2026)</title>
        <published>2007-06-09T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/mysql-remove-duplicate-rows/"/>
        <id>https://www.x2q.net/post/mysql-remove-duplicate-rows/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/mysql-remove-duplicate-rows/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; find duplicates with &lt;code&gt;GROUP BY … HAVING COUNT(*) &amp;gt; 1&lt;&#x2F;code&gt;; delete them with a self-join &lt;code&gt;DELETE&lt;&#x2F;code&gt; that keeps the lowest &lt;code&gt;id&lt;&#x2F;code&gt;; then add a &lt;code&gt;UNIQUE&lt;&#x2F;code&gt; index so they can’t return. The old &lt;code&gt;ALTER IGNORE TABLE … ADD UNIQUE&lt;&#x2F;code&gt; shortcut was &lt;strong&gt;removed in MySQL 5.7&lt;&#x2F;strong&gt; — don’t use it.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 2007 version of this post used &lt;code&gt;ALTER IGNORE TABLE … ADD UNIQUE INDEX&lt;&#x2F;code&gt; to let MySQL drop the dupes for you. That &lt;code&gt;IGNORE&lt;&#x2F;code&gt; clause is gone in modern MySQL, so here are the approaches that actually work in 2026.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;1-find-the-duplicates-first&quot;&gt;1. Find the duplicates first&lt;&#x2F;h2&gt;
&lt;p&gt;Decide what “duplicate” means — which columns must match. Say rows are duplicates when &lt;code&gt;(a, b)&lt;&#x2F;code&gt; are equal:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;SELECT a, b, COUNT(*) AS n
FROM mydata
GROUP BY a, b
HAVING n &amp;gt; 1;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Always look before you delete.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-delete-them-keeping-one-row&quot;&gt;2. Delete them, keeping one row&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;self-join-works-on-every-version&quot;&gt;Self-join (works on every version)&lt;&#x2F;h3&gt;
&lt;p&gt;Keep the row with the &lt;strong&gt;lowest &lt;code&gt;id&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; in each duplicate group:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;DELETE t1
FROM mydata t1
JOIN mydata t2
  ON t1.a = t2.a
 AND t1.b = t2.b
 AND t1.id &amp;gt; t2.id;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;t1.id &amp;gt; t2.id&lt;&#x2F;code&gt; deletes every copy except the smallest id. Flip to &lt;code&gt;&amp;lt;&lt;&#x2F;code&gt; to keep the newest instead.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;window-function-mysql-8-0-mariadb-10-2&quot;&gt;Window function (MySQL 8.0+ &#x2F; MariaDB 10.2+)&lt;&#x2F;h3&gt;
&lt;p&gt;Cleaner and easy to extend to “keep the newest per group”:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;DELETE FROM mydata
WHERE id IN (
  SELECT id FROM (
    SELECT id,
           ROW_NUMBER() OVER (PARTITION BY a, b ORDER BY id) AS rn
    FROM mydata
  ) x
  WHERE rn &amp;gt; 1
);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The extra subquery layer is required — MySQL won’t let you delete from a table you’re selecting from directly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-stop-them-coming-back&quot;&gt;3. Stop them coming back&lt;&#x2F;h2&gt;
&lt;p&gt;Once the table is clean, add a &lt;code&gt;UNIQUE&lt;&#x2F;code&gt; index so future inserts can’t duplicate:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;ALTER TABLE mydata ADD UNIQUE INDEX uq_a_b (a, b);
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then write inserts as &lt;code&gt;INSERT … ON DUPLICATE KEY UPDATE …&lt;&#x2F;code&gt; or &lt;code&gt;INSERT IGNORE&lt;&#x2F;code&gt; so the dedup is enforced at write time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;big-tables-the-rebuild-approach&quot;&gt;Big tables: the rebuild approach&lt;&#x2F;h2&gt;
&lt;p&gt;For very large tables, rewriting via a fresh table is often faster than a giant &lt;code&gt;DELETE&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE TABLE mydata_clean LIKE mydata;
ALTER TABLE mydata_clean ADD UNIQUE INDEX uq_a_b (a, b);
INSERT IGNORE INTO mydata_clean SELECT * FROM mydata;   -- IGNORE drops dup-key rows
RENAME TABLE mydata TO mydata_old, mydata_clean TO mydata;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;why-doesn-t-alter-ignore-table-work-anymore&quot;&gt;Why doesn’t ALTER IGNORE TABLE work anymore?&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;IGNORE&lt;&#x2F;code&gt; keyword on &lt;code&gt;ALTER TABLE&lt;&#x2F;code&gt; was deprecated in MySQL 5.6 and &lt;strong&gt;removed in 5.7&lt;&#x2F;strong&gt;. The self-join or rebuild approach replaces it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-keep-the-newest-row-instead-of-the-oldest&quot;&gt;How do I keep the newest row instead of the oldest?&lt;&#x2F;h3&gt;
&lt;p&gt;Self-join: change &lt;code&gt;t1.id &amp;gt; t2.id&lt;&#x2F;code&gt; to &lt;code&gt;t1.id &amp;lt; t2.id&lt;&#x2F;code&gt;. Window function: &lt;code&gt;ORDER BY id DESC&lt;&#x2F;code&gt; in the &lt;code&gt;PARTITION BY&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;back-up-first&quot;&gt;Back up first?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes. &lt;code&gt;DELETE&lt;&#x2F;code&gt; is irreversible — take a &lt;a href=&quot;&#x2F;post&#x2F;compress-mysqldump-output&#x2F;&quot;&gt;compressed dump&lt;&#x2F;a&gt; before running it on production.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Find: &lt;code&gt;GROUP BY cols HAVING COUNT(*) &amp;gt; 1&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Delete (portable): self-join &lt;code&gt;DELETE … WHERE t1.id &amp;gt; t2.id&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Delete (modern): &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY …)&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Prevent: add a &lt;code&gt;UNIQUE&lt;&#x2F;code&gt; index + &lt;code&gt;INSERT … ON DUPLICATE KEY&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SSH login without a password — set up key authentication (2026)</title>
        <published>2007-06-09T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/ssh-key-login-without-password/"/>
        <id>https://www.x2q.net/post/ssh-key-login-without-password/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/ssh-key-login-without-password/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;ssh-keygen -t ed25519&lt;&#x2F;code&gt;, then &lt;code&gt;ssh-copy-id user@host&lt;&#x2F;code&gt;. Now &lt;code&gt;ssh user@host&lt;&#x2F;code&gt; logs you in with no password. Use a passphrase on the key plus &lt;code&gt;ssh-agent&lt;&#x2F;code&gt; so it stays both convenient &lt;em&gt;and&lt;&#x2F;em&gt; safe.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the oldest how-tos from my blog (2007), back when &lt;code&gt;ssh-keygen -t rsa&lt;&#x2F;code&gt; was the answer. In 2026 you want &lt;strong&gt;ed25519&lt;&#x2F;strong&gt; keys, and &lt;code&gt;ssh-copy-id&lt;&#x2F;code&gt; does the fiddly part for you.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;&#x2F;h2&gt;
&lt;p&gt;Key authentication uses a &lt;strong&gt;key pair&lt;&#x2F;strong&gt;: a &lt;em&gt;private&lt;&#x2F;em&gt; key that never leaves your machine, and a matching &lt;em&gt;public&lt;&#x2F;em&gt; key you place on each server. The server challenges you with something only the private key can answer — so you prove who you are without ever sending a password.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;1-generate-a-key-pair&quot;&gt;1. Generate a key pair&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -C &amp;quot;you@laptop&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-t ed25519&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — modern, short, fast, secure. (Use &lt;code&gt;-t rsa -b 4096&lt;&#x2F;code&gt; only for ancient servers that don’t support ed25519.)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-C&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — a comment to identify the key later.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;You’ll be asked where to save it (default &lt;code&gt;~&#x2F;.ssh&#x2F;id_ed25519&lt;&#x2F;code&gt;) and for a &lt;strong&gt;passphrase&lt;&#x2F;strong&gt;. Use one — see the agent note below. This produces two files: &lt;code&gt;id_ed25519&lt;&#x2F;code&gt; (private — guard it) and &lt;code&gt;id_ed25519.pub&lt;&#x2F;code&gt; (public — safe to share).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-copy-the-public-key-to-the-server&quot;&gt;2. Copy the public key to the server&lt;&#x2F;h2&gt;
&lt;p&gt;The easy way:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id user@remote.host
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It appends your public key to &lt;code&gt;~&#x2F;.ssh&#x2F;authorized_keys&lt;&#x2F;code&gt; on the server and fixes the permissions. If &lt;code&gt;ssh-copy-id&lt;&#x2F;code&gt; isn’t available, do it by hand:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;cat ~&#x2F;.ssh&#x2F;id_ed25519.pub | ssh user@remote.host \
  &amp;quot;mkdir -p ~&#x2F;.ssh &amp;amp;&amp;amp; chmod 700 ~&#x2F;.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~&#x2F;.ssh&#x2F;authorized_keys &amp;amp;&amp;amp; chmod 600 ~&#x2F;.ssh&#x2F;authorized_keys&amp;quot;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;3-log-in&quot;&gt;3. Log in&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;ssh user@remote.host
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No password prompt (just your key’s passphrase the first time, if you set one).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keep-it-convenient-with-ssh-agent&quot;&gt;Keep it convenient with ssh-agent&lt;&#x2F;h2&gt;
&lt;p&gt;A passphrase-protected key would defeat the “no password” goal — except &lt;code&gt;ssh-agent&lt;&#x2F;code&gt; caches the unlocked key for your session, so you type the passphrase &lt;strong&gt;once&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;eval &amp;quot;$(ssh-agent -s)&amp;quot;     # start the agent (often already running)
ssh-add ~&#x2F;.ssh&#x2F;id_ed25519  # unlock the key once
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;macOS stores it in the Keychain automatically with &lt;code&gt;ssh-add --apple-use-keychain&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;optionally-turn-off-password-logins&quot;&gt;Optionally: turn off password logins&lt;&#x2F;h2&gt;
&lt;p&gt;Once keys work, disabling password auth shuts the door on brute-force attempts entirely. On the server, edit &lt;code&gt;&#x2F;etc&#x2F;ssh&#x2F;sshd_config&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;PasswordAuthentication no
PubkeyAuthentication yes
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then reload: &lt;code&gt;sudo systemctl reload ssh&lt;&#x2F;code&gt; (or &lt;code&gt;sshd&lt;&#x2F;code&gt;). &lt;strong&gt;Confirm key login works in a second session before you do this&lt;&#x2F;strong&gt;, or you can lock yourself out.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;permissions-matter-the-1-reason-it-doesn-t-work&quot;&gt;Permissions matter (the #1 reason it “doesn’t work”)&lt;&#x2F;h2&gt;
&lt;p&gt;SSH refuses keys if the permissions are too loose:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chmod 700 ~&#x2F;.ssh
chmod 600 ~&#x2F;.ssh&#x2F;authorized_keys      # on the server
chmod 600 ~&#x2F;.ssh&#x2F;id_ed25519           # your private key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;still-asks-for-a-password&quot;&gt;Still asks for a password?&lt;&#x2F;h3&gt;
&lt;p&gt;Almost always permissions (above) or the wrong key. Debug with &lt;code&gt;ssh -v user@host&lt;&#x2F;code&gt; and watch which key it offers and whether the server rejects &lt;code&gt;authorized_keys&lt;&#x2F;code&gt; for being group&#x2F;world-writable.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;multiple-keys-multiple-servers&quot;&gt;Multiple keys &#x2F; multiple servers?&lt;&#x2F;h3&gt;
&lt;p&gt;Put them in &lt;code&gt;~&#x2F;.ssh&#x2F;config&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Host prod
  HostName prod.example.com
  User deploy
  IdentityFile ~&#x2F;.ssh&#x2F;id_ed25519
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then just &lt;code&gt;ssh prod&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rsa-or-ed25519&quot;&gt;rsa or ed25519?&lt;&#x2F;h3&gt;
&lt;p&gt;ed25519 by default — smaller, faster, and secure. Reach for &lt;code&gt;rsa -b 4096&lt;&#x2F;code&gt; only when talking to an old server that lacks ed25519 support.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ssh-keygen -t ed25519&lt;&#x2F;code&gt; → &lt;code&gt;ssh-copy-id user@host&lt;&#x2F;code&gt; → &lt;code&gt;ssh user@host&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Use a passphrase + &lt;code&gt;ssh-agent&lt;&#x2F;code&gt; to stay convenient and safe.&lt;&#x2F;li&gt;
&lt;li&gt;Fix &lt;code&gt;~&#x2F;.ssh&lt;&#x2F;code&gt; permissions (700&#x2F;600) if it won’t take the key.&lt;&#x2F;li&gt;
&lt;li&gt;Set &lt;code&gt;PasswordAuthentication no&lt;&#x2F;code&gt; once keys work — after testing.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Convert a text file&#x27;s character encoding with iconv (2026)</title>
        <published>2007-02-24T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/convert-file-encoding-iconv/"/>
        <id>https://www.x2q.net/post/convert-file-encoding-iconv/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/convert-file-encoding-iconv/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;iconv -f UTF-8 -t ISO-8859-1 in.txt &amp;gt; out.txt&lt;&#x2F;code&gt; converts encodings. Check what you’ve got first with &lt;code&gt;file -i in.txt&lt;&#x2F;code&gt;, and add &lt;code&gt;&#x2F;&#x2F;TRANSLIT&lt;&#x2F;code&gt; (approximate) or &lt;code&gt;&#x2F;&#x2F;IGNORE&lt;&#x2F;code&gt; (drop) for characters the target can’t represent.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 2007 note — and the original even had the direction muddled, which is exactly the trap. &lt;code&gt;iconv&lt;&#x2F;code&gt; does nothing to &lt;em&gt;tell&lt;&#x2F;em&gt; you which way to go, so step one is always: find out what encoding you actually have.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;1-detect-the-current-encoding&quot;&gt;1. Detect the current encoding&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;file -i mystery.txt
mystery.txt: text&#x2F;plain; charset=utf-8
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;file&lt;&#x2F;code&gt; guesses from the bytes — usually right for UTF-8 vs Latin-1, but a guess. If Danish &lt;code&gt;æ ø å&lt;&#x2F;code&gt; (or other non-ASCII) shows up garbled in your editor, the declared&#x2F;assumed encoding is wrong.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-convert&quot;&gt;2. Convert&lt;&#x2F;h2&gt;
&lt;p&gt;The flags are &lt;strong&gt;&lt;code&gt;-f&lt;&#x2F;code&gt; (from)&lt;&#x2F;strong&gt; and &lt;strong&gt;&lt;code&gt;-t&lt;&#x2F;code&gt; (to)&lt;&#x2F;strong&gt;. Don’t mix them up:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# UTF-8  -&amp;gt;  ISO-8859-1 (Latin-1)
iconv -f UTF-8 -t ISO-8859-1 utf.txt &amp;gt; latin1.txt

# ISO-8859-1  -&amp;gt;  UTF-8
iconv -f ISO-8859-1 -t UTF-8 latin1.txt &amp;gt; utf.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(The long forms &lt;code&gt;--from-code&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;--to-code&lt;&#x2F;code&gt; are identical.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-handle-characters-the-target-can-t-represent&quot;&gt;3. Handle characters the target can’t represent&lt;&#x2F;h2&gt;
&lt;p&gt;Converting &lt;em&gt;to&lt;&#x2F;em&gt; a narrow charset like ISO-8859-1 fails on characters it doesn’t contain (€, em-dashes, emoji) with &lt;code&gt;illegal input sequence&lt;&#x2F;code&gt;. Two ways out:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;# &#x2F;&#x2F;TRANSLIT — approximate: “ ” -&amp;gt; &amp;quot; &amp;quot;, é -&amp;gt; e where needed
iconv -f UTF-8 -t ISO-8859-1&#x2F;&#x2F;TRANSLIT in.txt &amp;gt; out.txt

# &#x2F;&#x2F;IGNORE — silently drop characters that don&amp;#39;t fit
iconv -f UTF-8 -t ISO-8859-1&#x2F;&#x2F;IGNORE in.txt &amp;gt; out.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;edit-in-place-safely&quot;&gt;Edit in place safely&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;iconv in.txt &amp;gt; in.txt&lt;&#x2F;code&gt; truncates the file before reading it — you lose the data. Use a temp file:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;iconv -f ISO-8859-1 -t UTF-8 in.txt &amp;gt; in.tmp &amp;amp;&amp;amp; mv in.tmp in.txt
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;batch-convert-a-tree&quot;&gt;Batch-convert a tree&lt;&#x2F;h2&gt;
&lt;p&gt;Convert every &lt;code&gt;.txt&lt;&#x2F;code&gt; under a directory from Latin-1 to UTF-8:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;find . -type f -name &amp;#39;*.txt&amp;#39; -exec sh -c \
  &amp;#39;iconv -f ISO-8859-1 -t UTF-8 &amp;quot;$1&amp;quot; &amp;gt; &amp;quot;$1.utf8&amp;quot; &amp;amp;&amp;amp; mv &amp;quot;$1.utf8&amp;quot; &amp;quot;$1&amp;quot;&amp;#39; _ {} \;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;List everything &lt;code&gt;iconv&lt;&#x2F;code&gt; supports with &lt;code&gt;iconv -l&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;iconv-says-illegal-input-sequence-at-position-n&quot;&gt;iconv says “illegal input sequence at position N”&lt;&#x2F;h3&gt;
&lt;p&gt;The real source encoding isn’t what you told &lt;code&gt;-f&lt;&#x2F;code&gt;, or you’re converting to a charset that can’t hold a character. Re-check with &lt;code&gt;file -i&lt;&#x2F;code&gt;, or add &lt;code&gt;&#x2F;&#x2F;TRANSLIT&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;&#x2F;&#x2F;IGNORE&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-do-i-strip-a-utf-8-bom&quot;&gt;How do I strip a UTF-8 BOM?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;iconv&lt;&#x2F;code&gt; won’t always remove it; &lt;code&gt;sed &#x27;1s&#x2F;^\xEF\xBB\xBF&#x2F;&#x2F;&#x27; in.txt &amp;gt; out.txt&lt;&#x2F;code&gt; drops a leading BOM.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-about-windows-line-endings-while-i-m-at-it&quot;&gt;What about Windows line endings while I’m at it?&lt;&#x2F;h3&gt;
&lt;p&gt;Encoding and line endings are separate problems — see &lt;a href=&quot;&#x2F;post&#x2F;dos2unix-convert-line-endings&#x2F;&quot;&gt;dos2unix&lt;&#x2F;a&gt; for CRLF→LF.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Detect: &lt;code&gt;file -i file.txt&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Convert: &lt;code&gt;iconv -f FROM -t TO in &amp;gt; out&lt;&#x2F;code&gt; (&lt;code&gt;-f&lt;&#x2F;code&gt; = from, &lt;code&gt;-t&lt;&#x2F;code&gt; = to).&lt;&#x2F;li&gt;
&lt;li&gt;Lossy targets: append &lt;code&gt;&#x2F;&#x2F;TRANSLIT&lt;&#x2F;code&gt; or &lt;code&gt;&#x2F;&#x2F;IGNORE&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;In place: go via a temp file, never &lt;code&gt;&amp;gt; same-file&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Remove the passphrase from a private key with OpenSSL (2026)</title>
        <published>2006-09-03T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/openssl-remove-passphrase-private-key/"/>
        <id>https://www.x2q.net/post/openssl-remove-passphrase-private-key/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/openssl-remove-passphrase-private-key/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;openssl rsa -in server.key -out server.key.nopass&lt;&#x2F;code&gt; writes a passphrase-free copy of an RSA key (use &lt;code&gt;openssl pkey&lt;&#x2F;code&gt; for any key type). Then &lt;code&gt;chmod 600&lt;&#x2F;code&gt; it. The server will start without prompting — at the cost of an unencrypted key on disk, so guard it.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Refreshed from a 2006 note. The mechanism is unchanged; the commands below add the EC and modern any-key forms, and the security caveat it really deserves.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;&#x2F;h2&gt;
&lt;p&gt;If your TLS private key is passphrase-encrypted, the web server can’t start unattended — it stops and waits for someone to type the passphrase. That breaks reboots, autoscaling, and config-management. The fix is an &lt;strong&gt;unencrypted&lt;&#x2F;strong&gt; copy of the key.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;remove-it&quot;&gt;Remove it&lt;&#x2F;h2&gt;
&lt;p&gt;For a classic RSA key:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -in server.key -out server.key.nopass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For an EC (elliptic-curve) key:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl ec -in server.key -out server.key.nopass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Type-agnostic (works for RSA, EC, Ed25519 — the modern catch-all):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl pkey -in server.key -out server.key.nopass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;OpenSSL prompts for the current passphrase, then writes the decrypted key. Point your server at the &lt;code&gt;.nopass&lt;&#x2F;code&gt; file and it’ll start silently.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lock-the-file-down&quot;&gt;Lock the file down&lt;&#x2F;h2&gt;
&lt;p&gt;An unencrypted key is a plaintext credential. At minimum:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;chmod 600 server.key.nopass
chown root:root server.key.nopass
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Anyone who reads this file owns your certificate’s identity until it’s revoked. Treat it accordingly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;going-the-other-way-add-a-passphrase&quot;&gt;Going the other way: add a passphrase&lt;&#x2F;h2&gt;
&lt;p&gt;To &lt;em&gt;encrypt&lt;&#x2F;em&gt; a key (e.g. before moving it):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -aes256 -in plain.key -out encrypted.key
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;should-you-actually-do-this&quot;&gt;Should you actually do this?&lt;&#x2F;h2&gt;
&lt;p&gt;Removing the passphrase is convenient but trades away a layer of protection — a stolen disk image now hands over a working key. Consider the alternatives before reaching for &lt;code&gt;.nopass&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;systemd &lt;code&gt;ask-password&lt;&#x2F;code&gt; &#x2F; a key-loading agent&lt;&#x2F;strong&gt; can supply the passphrase at boot without baking it in.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A secrets manager&lt;&#x2F;strong&gt; (Vault, cloud KMS, or the platform’s secret store) hands the key to the process at runtime and never persists it in the clear.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Tight file permissions + full-disk encryption&lt;&#x2F;strong&gt; mitigate the plaintext-on-disk risk if you must use an unencrypted key.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a single hobby server, &lt;code&gt;.nopass&lt;&#x2F;code&gt; + &lt;code&gt;chmod 600&lt;&#x2F;code&gt; is a reasonable, honest trade. For anything sensitive, push the passphrase&#x2F;secret out of the filesystem.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;will-the-certificate-still-match-after-i-do-this&quot;&gt;Will the certificate still match after I do this?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — you’re only changing how the key is &lt;em&gt;stored&lt;&#x2F;em&gt;, not the key itself. The public key, certificate, and CSR all still match. (Verify with the modulus check in &lt;a href=&quot;&#x2F;post&#x2F;inspect-verify-tls-certificate-openssl&#x2F;&quot;&gt;inspecting &amp;amp; verifying certificates&lt;&#x2F;a&gt;.)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nginx-apache-still-prompts-after-i-switched-files&quot;&gt;nginx&#x2F;Apache still prompts after I switched files?&lt;&#x2F;h3&gt;
&lt;p&gt;You’re still pointing at the encrypted key. Check &lt;code&gt;ssl_certificate_key&lt;&#x2F;code&gt; (nginx) &#x2F; &lt;code&gt;SSLCertificateKeyFile&lt;&#x2F;code&gt; (Apache) really points at the &lt;code&gt;.nopass&lt;&#x2F;code&gt; file, then reload.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;can-i-tell-whether-a-key-is-encrypted&quot;&gt;Can I tell whether a key is encrypted?&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;head -1 server.key&lt;&#x2F;code&gt; — an encrypted PEM shows &lt;code&gt;Proc-Type: 4,ENCRYPTED&lt;&#x2F;code&gt; or a &lt;code&gt;BEGIN ENCRYPTED PRIVATE KEY&lt;&#x2F;code&gt; header.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;RSA: &lt;code&gt;openssl rsa -in server.key -out server.key.nopass&lt;&#x2F;code&gt;; any key: &lt;code&gt;openssl pkey …&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;chmod 600&lt;&#x2F;code&gt; and restrict ownership — it’s now a plaintext credential.&lt;&#x2F;li&gt;
&lt;li&gt;Prefer a boot-time passphrase supplier or secrets manager for anything that matters.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>MySQL: set or reset the AUTO_INCREMENT value (2026)</title>
        <published>2006-08-31T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/mysql-set-auto-increment/"/>
        <id>https://www.x2q.net/post/mysql-set-auto-increment/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/mysql-set-auto-increment/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; &lt;code&gt;ALTER TABLE t AUTO_INCREMENT = 100000;&lt;&#x2F;code&gt; sets the next auto-increment value. You can raise it freely, but you &lt;strong&gt;can’t set it below the current maximum id&lt;&#x2F;strong&gt; — MySQL silently clamps it to max+1.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the shortest notes from the old blog (2006) — one line. Still correct, but the caveats below are what actually trip people up.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;set-it&quot;&gt;Set it&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;ALTER TABLE mytable AUTO_INCREMENT = 100000;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The next inserted row gets id &lt;code&gt;100000&lt;&#x2F;code&gt; (assuming that’s above the current max). Handy for leaving room between data sets, or starting ids at a friendlier number.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reset-after-deleting-rows&quot;&gt;Reset after deleting rows&lt;&#x2F;h2&gt;
&lt;p&gt;Deleted the tail of a table and want ids to continue from the real maximum instead of the old high-water mark?&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;-- continue from the highest existing id
ALTER TABLE mytable AUTO_INCREMENT = 1;   -- clamps up to max(id)+1 automatically
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Setting it to &lt;code&gt;1&lt;&#x2F;code&gt; doesn’t reset to 1 if rows exist — MySQL clamps to &lt;code&gt;MAX(id) + 1&lt;&#x2F;code&gt;. That’s usually exactly what you want after a cleanup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;empty-a-table-and-truly-start-at-1&quot;&gt;Empty a table and truly start at 1&lt;&#x2F;h2&gt;
&lt;p&gt;If you want ids to restart from 1, the table must be empty. &lt;code&gt;TRUNCATE&lt;&#x2F;code&gt; does both — wipes rows &lt;em&gt;and&lt;&#x2F;em&gt; resets the counter:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;TRUNCATE TABLE mytable;     -- removes all rows AND resets AUTO_INCREMENT to 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(&lt;code&gt;DELETE FROM mytable&lt;&#x2F;code&gt; removes rows but leaves the counter where it was — use &lt;code&gt;ALTER TABLE … AUTO_INCREMENT = 1&lt;&#x2F;code&gt; after it, or &lt;code&gt;TRUNCATE&lt;&#x2F;code&gt;.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Can’t go below the max.&lt;&#x2F;strong&gt; &lt;code&gt;ALTER TABLE … AUTO_INCREMENT = 5&lt;&#x2F;code&gt; on a table whose largest id is 900 leaves it at 901. There’s no way to reuse ids below existing data without rewriting the table.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Engine differences on restart.&lt;&#x2F;strong&gt; Modern InnoDB (MySQL 8.0+) persists the counter across restarts. Older InnoDB (≤5.7) recomputed it as &lt;code&gt;MAX(id)+1&lt;&#x2F;code&gt; at startup, so a gap at the top could “shrink” after a reboot. MyISAM always persisted it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Replication &#x2F; Galera.&lt;&#x2F;strong&gt; With &lt;code&gt;auto_increment_increment&lt;&#x2F;code&gt; &amp;gt; 1 (multi-primary, Galera, group replication), ids jump in steps (e.g. 1, 3, 5…) by design — don’t “fix” the gaps, they prevent id collisions between nodes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Gaps are normal.&lt;&#x2F;strong&gt; Rolled-back transactions and failed inserts consume ids. Auto-increment guarantees uniqueness and monotonicity, &lt;strong&gt;not&lt;&#x2F;strong&gt; contiguity. Don’t rely on “no holes”.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;how-do-i-see-the-current-value&quot;&gt;How do I see the current value?&lt;&#x2F;h3&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;SHOW TABLE STATUS LIKE &amp;#39;mytable&amp;#39;;   -- the Auto_increment column
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or query &lt;code&gt;information_schema.TABLES&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;it-won-t-let-me-lower-it-why&quot;&gt;It won’t let me lower it — why?&lt;&#x2F;h3&gt;
&lt;p&gt;By design: lowering below existing ids would risk duplicate keys. Rewrite the table (dump, recreate, reload) if you genuinely must renumber.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;does-this-work-the-same-in-mariadb&quot;&gt;Does this work the same in MariaDB?&lt;&#x2F;h3&gt;
&lt;p&gt;Yes — &lt;code&gt;ALTER TABLE … AUTO_INCREMENT = N&lt;&#x2F;code&gt; and &lt;code&gt;TRUNCATE&lt;&#x2F;code&gt; behave the same. Restart-persistence depends on the engine&#x2F;version as above.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Set&#x2F;raise: &lt;code&gt;ALTER TABLE t AUTO_INCREMENT = N&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;After deletes: &lt;code&gt;ALTER TABLE t AUTO_INCREMENT = 1&lt;&#x2F;code&gt; clamps to &lt;code&gt;MAX(id)+1&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Restart at 1: &lt;code&gt;TRUNCATE TABLE t&lt;&#x2F;code&gt; (must be empty).&lt;&#x2F;li&gt;
&lt;li&gt;Can’t set below the current max; gaps are expected.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Java cacerts default password — and how to use the truststore (2026)</title>
        <published>2006-08-29T00:00:00+00:00</published>
        <updated>2026-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.x2q.net/post/java-keystore-default-password-cacerts/"/>
        <id>https://www.x2q.net/post/java-keystore-default-password-cacerts/</id>
        
        <content type="html" xml:base="https://www.x2q.net/post/java-keystore-default-password-cacerts/">&lt;p&gt;&lt;strong&gt;TL;DR —&lt;&#x2F;strong&gt; the default password for Java’s &lt;code&gt;cacerts&lt;&#x2F;code&gt; truststore is &lt;strong&gt;&lt;code&gt;changeit&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;. The file lives at &lt;code&gt;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&lt;&#x2F;code&gt; in any modern (Java 9+) JDK. Use &lt;code&gt;keytool&lt;&#x2F;code&gt; to list, import, and change the password.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;This was a two-line note in 2006 (the password and the path). Both are still right — Java never changed the default — but the path moved in Java 9, and the useful part is what you &lt;em&gt;do&lt;&#x2F;em&gt; with the truststore, below.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-default-password&quot;&gt;The default password&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;changeit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That’s it. It’s the same on every JDK, every OS, and has been for the entire history of Java. (Some Linux distros that manage the system truststore set it to &lt;code&gt;changeme&lt;&#x2F;code&gt; — if &lt;code&gt;changeit&lt;&#x2F;code&gt; is rejected, try that.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-cacerts-lives&quot;&gt;Where cacerts lives&lt;&#x2F;h2&gt;
&lt;p&gt;In a modern JDK (Java 9 and later, after the JRE&#x2F;JDK merge):&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On older Java 8 and earlier it was under &lt;code&gt;$JAVA_HOME&#x2F;jre&#x2F;lib&#x2F;security&#x2F;cacerts&lt;&#x2F;code&gt;. Find it if unsure:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;find &amp;quot;$JAVA_HOME&amp;quot; -name cacerts
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;list-the-trusted-cas&quot;&gt;List the trusted CAs&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;keytool -list -keystore &amp;quot;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&amp;quot; -storepass changeit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Add &lt;code&gt;-v&lt;&#x2F;code&gt; for full detail, or grep for a specific alias.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;import-a-certificate-internal-ca-self-signed&quot;&gt;Import a certificate (internal CA &#x2F; self-signed)&lt;&#x2F;h2&gt;
&lt;p&gt;The common reason you’re here: a Java app throws &lt;code&gt;PKIX path building failed&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;unable to find valid certification path&lt;&#x2F;code&gt; because it doesn’t trust an internal or self-signed cert. Import it into the truststore:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;keytool -importcert \
  -alias my-internal-ca \
  -file internal-ca.crt \
  -keystore &amp;quot;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&amp;quot; \
  -storepass changeit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Grab the cert a server is presenting first if you don’t have the file — see &lt;a href=&quot;&#x2F;post&#x2F;inspect-verify-tls-certificate-openssl&#x2F;&quot;&gt;inspecting a TLS certificate&lt;&#x2F;a&gt; (&lt;code&gt;openssl s_client … -showcerts&lt;&#x2F;code&gt;). Prefer importing the &lt;strong&gt;CA&lt;&#x2F;strong&gt; that signed the cert over the leaf cert itself, so it keeps working after renewal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;remove-or-replace-an-entry&quot;&gt;Remove or replace an entry&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code&gt;keytool -delete -alias my-internal-ca \
  -keystore &amp;quot;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&amp;quot; -storepass changeit
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;actually-change-the-password&quot;&gt;Actually change the password&lt;&#x2F;h2&gt;
&lt;p&gt;The alias is literally “changeit” for a reason:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;keytool -storepasswd \
  -keystore &amp;quot;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&amp;quot; \
  -storepass changeit -new &amp;lt;new-password&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In practice most setups leave the global &lt;code&gt;cacerts&lt;&#x2F;code&gt; at the default and instead point apps at a &lt;strong&gt;separate truststore&lt;&#x2F;strong&gt; they control (&lt;code&gt;-Djavax.net.ssl.trustStore=…&lt;&#x2F;code&gt;), which is cleaner than editing the JDK’s own file.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;should-i-edit-the-jdk-s-cacerts-directly&quot;&gt;Should I edit the JDK’s cacerts directly?&lt;&#x2F;h3&gt;
&lt;p&gt;For a quick fix, sure. Better practice: keep a project-specific truststore and pass &lt;code&gt;-Djavax.net.ssl.trustStore=&#x2F;path&#x2F;to&#x2F;store -Djavax.net.ssl.trustStorePassword=…&lt;&#x2F;code&gt;, so a JDK upgrade doesn’t wipe your changes.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keytool-says-keystore-was-tampered-with-or-password-was-incorrect&quot;&gt;keytool says “keystore was tampered with, or password was incorrect”&lt;&#x2F;h3&gt;
&lt;p&gt;Wrong password — try &lt;code&gt;changeit&lt;&#x2F;code&gt;, then &lt;code&gt;changeme&lt;&#x2F;code&gt; (distro-managed JDKs). Confirm you’re pointing at the right &lt;code&gt;cacerts&lt;&#x2F;code&gt; if you have multiple JDKs installed.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;truststore-vs-keystore-what-s-the-difference&quot;&gt;Truststore vs keystore — what’s the difference?&lt;&#x2F;h3&gt;
&lt;p&gt;A &lt;strong&gt;truststore&lt;&#x2F;strong&gt; holds the CA certs you trust (cacerts). A &lt;strong&gt;keystore&lt;&#x2F;strong&gt; holds &lt;em&gt;your&lt;&#x2F;em&gt; private keys + certs (what a server presents). Same file format, opposite roles.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;Default &lt;code&gt;cacerts&lt;&#x2F;code&gt; password: &lt;strong&gt;&lt;code&gt;changeit&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; (sometimes &lt;code&gt;changeme&lt;&#x2F;code&gt; on distro JDKs).&lt;&#x2F;li&gt;
&lt;li&gt;Path (Java 9+): &lt;code&gt;$JAVA_HOME&#x2F;lib&#x2F;security&#x2F;cacerts&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;keytool -list&lt;&#x2F;code&gt; to view, &lt;code&gt;-importcert&lt;&#x2F;code&gt; to trust an internal CA, &lt;code&gt;-storepasswd&lt;&#x2F;code&gt; to change it.&lt;&#x2F;li&gt;
&lt;li&gt;Prefer a separate app-managed truststore over editing the JDK’s own.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
</feed>
