<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://vito.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vito.io/" rel="alternate" type="text/html" /><updated>2026-03-13T11:56:23-03:00</updated><id>https://vito.io/feed.xml</id><title type="html">Vito Sartori</title><entry><title type="html">Rebuilding Windows Live Messenger, Part 3: The Specification</title><link href="https://vito.io/articles/2026-02-19-the-specification.html" rel="alternate" type="text/html" title="Rebuilding Windows Live Messenger, Part 3: The Specification" /><published>2026-02-19T00:00:00-03:00</published><updated>2026-02-19T00:00:00-03:00</updated><id>https://vito.io/articles/the-specification</id><content type="html" xml:base="https://vito.io/articles/2026-02-19-the-specification.html"><![CDATA[<p>This post continues the investigation that began with
“<a href="/articles/2026-02-15-this-is-not-rle">This is not RLE</a>”.</p>

<p>If the first post was about dismantling assumptions, this one is about replacing
them with certainty. Back then, we had a decoder that worked, a pile of strange
patterns, and a growing suspicion that the name <em>RLE</em> was little more than a
historical artifact. Now, after walking the bitstream one bit at a time and
validating every rule against a real corpus, the format is no longer a black box.
It has structure, semantics, and — finally — a specification.</p>

<p>When I started digging into this format, I expected some variation of classic
run-length encoding. What I found instead was closer to a tiny virtual machine
for pixels. Yes, there are runs, but there are also signed deltas, literal
spans, and a single bit-cursor shared across colour channels. Nothing is
byte-aligned, nor accidental: every bit participates in reconstructing
UI artwork that was never meant to be seen outside its original renderer.</p>

<h2 id="the-turning-point-transparency">The Turning Point: Transparency</h2>

<p>Up until then, decoding felt linear: read tokens, expand pixels, move forward.
Alpha changed the rules. Transparent spans emit pixels without consuming colour
data. Buffers persist across segments; the “previous value” never truly resets.
Suddenly the format stopped behaving like a simple stream and revealed itself as
a stateful decoding model layered on top of a bit-level language. Once that
behaviour became clear, the rest of the structure — row navigation,
token width <code class="language-plaintext highlighter-rouge">B</code>, palette post-processing — just snapped into place.</p>

<h2 id="verification">Verification</h2>

<p>Hundreds of files were decoded and compared pixel-for-pixel against the original
renderer. No heuristics, no guesses — just reproducible behaviour. At that
point, the reverse engineering effort had effectively crossed a boundary: the
goal was no longer to make a decoder work, but to describe why it works.</p>

<p>So instead of shipping another tool, I wrote the document I wish I had at the
beginning: a complete, clean-room specification of the format. It defines the
container, the payload, the bitstream grammar, channel interleaving, the alpha
meta-decoder, palette semantics, DPI scaling, and the precise arithmetic rules
that govern decoding. Every field is explained, every algorithm is explicit.
Every edge case that once looked like noise now has a name.</p>

<p>The format is no longer folklore, no longer that “strange Messenger
blob.” It is a fully described image format with a deterministic decoding
model — and you can read it here:</p>

<p><a href="/assets/pdf/rle-dib-format-spec/rle-dib-format-spec.pdf">RLE-DIB Format Specification</a></p>

<p>The mystery is over. From here on, this format is something one can implement,
reason about, and build against — and, perhaps most satisfying of all, the
decoder is now far smaller than the puzzle that led to it. I validated the
spec by implementing a Swift parser — aside from the Python parser included in
the spec itself — that faithfully rendered assets on macOS.</p>

<h2 id="legal--research-disclaimer">Legal &amp; Research Disclaimer</h2>

<p>This material is intended strictly for academic research, reverse engineering
for interoperability, and software preservation.</p>

<p>The author does not distribute, reproduce, or provide any proprietary software,
including components originally developed by Microsoft.</p>

<p>This work is independent, clean-room in nature, and is not affiliated with,
endorsed by, or supported by any original vendor.</p>

<p>Any trademarks, formats, or software referenced remain the property of their
respective owners.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This post continues the investigation that began with “This is not RLE”.]]></summary></entry><entry><title type="html">Rebuilding Windows Live Messenger, Part 2: Messenger on macOS</title><link href="https://vito.io/articles/2026-02-17-messenger-on-mac.html" rel="alternate" type="text/html" title="Rebuilding Windows Live Messenger, Part 2: Messenger on macOS" /><published>2026-02-17T00:00:00-03:00</published><updated>2026-02-17T00:00:00-03:00</updated><id>https://vito.io/articles/messenger-on-mac</id><content type="html" xml:base="https://vito.io/articles/2026-02-17-messenger-on-mac.html"><![CDATA[<p>Just to take a breath between Assembly and graphics, I installed Mojave on
an old Mac Mini and spun Microsoft Messenger, which is the equivalent of Windows
Live Messenger for macOS, as removing Windows from the equation makes things
way easier. The objective today is to fool it enough to make it think it is
talking with the now long gone Microsoft servers.</p>

<p>The first step to make this way easier is to enable <em>logging for
troubleshooting</em>, which happily dumps really interesting stuff into
<code class="language-plaintext highlighter-rouge">~/Library/Logs/Microsoft-Messenger.log</code>.</p>

<h2 id="the-login-dance">The Login Dance</h2>

<p>Once an email and password is provided, the client immediately opens a raw TCP
connection to <code class="language-plaintext highlighter-rouge">messenger.hotmail.com</code>, port <code class="language-plaintext highlighter-rouge">1863</code>. This seems to be the server
that speaks the MSNP protocol, given the very first message from the client:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VER 1 MSNP16 MSNP15 MSNP14 MSNP13 CVR0
</code></pre></div></div>

<p>The protocol is fairly simple: text-based, each message ending in CR-LF. It has
the following format:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  VER 1 MSNP16 MSNP15 MSNP14 MSNP13 CVR0
  ━┳━ ┳ ━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   │  │  │
   │  │  └──▶ Payload
   │  └──▶ Message ID
   └──▶ Operation
</code></pre></div></div>

<p>The message begins with an operation code, followed by the message ID, and a
payload. The server always responds with the same message ID, and a
corresponding operation code and payload.</p>

<p>The <code class="language-plaintext highlighter-rouge">VER</code> operation is a handshake between the client and server. The client
announces the protocols it supports, and the server answers with one of the
protocols it supports. So the fake server can just pick the highest one and be
done with it.</p>

<p>I suppose <code class="language-plaintext highlighter-rouge">CVR</code> stands for two different things during the flow. When sent by
the client, it may be a <em>Client Version Report</em>, meaning “I use the base CVR
capability schema, revision zero”. For the server reply, CVR may be a
<em>Capability Vector Revision</em>, but I can’t confirm that. Echoing the <code class="language-plaintext highlighter-rouge">CVR0</code> back
to the client is enough to make it happy.</p>

<p>This leads to the following response from the server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VER 1 MSNP16 CVR0
</code></pre></div></div>

<p>Once the version is agreed between parties, the communication continues with
the client sending:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CVR 2 0x0409 mac 10.14.6 i386 macmsgs 8.0.1 macmsgs hey@vito.io
</code></pre></div></div>

<p>This is the <em>Client Version Report</em>, containing the following fields:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">0x0409</code>: Microsoft’s LCID (Locale ID) in Hex. <code class="language-plaintext highlighter-rouge">0x0409</code> is simply <code class="language-plaintext highlighter-rouge">en-US</code>; Italian, for instance would be <code class="language-plaintext highlighter-rouge">0x0410</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">mac</code>: Platform/OS family.</li>
  <li><code class="language-plaintext highlighter-rouge">10.14.6</code>: OS version string.</li>
  <li><code class="language-plaintext highlighter-rouge">i386</code>: Architecture. This seems to be hardcoded in the binary.</li>
  <li><code class="language-plaintext highlighter-rouge">macmsgs</code>: Client product identifier. Just identifies as Microsoft Messenger for Mac.</li>
  <li><code class="language-plaintext highlighter-rouge">8.0.1</code>: The client version.</li>
  <li><code class="language-plaintext highlighter-rouge">macmsgs</code>: Again. Not sure why.</li>
  <li><code class="language-plaintext highlighter-rouge">hey@vito.io</code>: User identity. That’s me!</li>
</ul>

<p>Even though the client waits for a response, what the server returns doesn’t seem to be used at all. My theory is that this may be just
telemetry, or a mechanism for Microsoft to block old clients or architectures.</p>

<p>For now the fake server is replying with <code class="language-plaintext highlighter-rouge">CVR 2 1 2 3 4 5</code>, and Messenger is
happy. After getting the reply, the client then starts the real authentication
flow:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>USR 3 TWN I hey@vito.io
</code></pre></div></div>

<p>From observation, <code class="language-plaintext highlighter-rouge">USR</code> is the User Authentication OP. Decoding it:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">TWN</code>: I assume this means <em>Ticket With Nexus</em>, considering what happens
later.</li>
  <li><code class="language-plaintext highlighter-rouge">I</code>: Seems to be the authentication phase the client is currently at. <code class="language-plaintext highlighter-rouge">I</code> may
indicate <em>Initiate</em>? <em>Initial</em>?</li>
  <li><code class="language-plaintext highlighter-rouge">hey@vito.io</code>: That me, again! This just includes whatever the user inputs in
the login “Email” field.</li>
</ul>

<p>The server then replies with some metadata that’s opaquely handled by the client
and appended to an HTTP <code class="language-plaintext highlighter-rouge">Authorization</code> header. So the server can reply with
literally anything:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>USR 3 TWN S fake=1
</code></pre></div></div>

<p>Then things shifts a little:</p>

<h3 id="the-passport-backend">The Passport Backend</h3>

<p>While still connected to the TCP server, the client opens a separate connection entirely – a concurrent, out-of-band flow to the defunct <em>Passport.net</em> server. Not without caveats, though.</p>

<p>All communication with <em>Passport.net</em> uses TLS – Actually, not TLS. SSL. – A
really old one: SSLv2 Hello, TLS 1.0. And the best part is that OpenSSL dropped
that a while ago. What I need now is a CA Root installed on Mojave, and a TLS
terminator so I can continue observing traffic.</p>

<p>So I got a certificate for <code class="language-plaintext highlighter-rouge">nexus.passport.com</code>, and wrote a little TLS shim
in C to route TLS traffic from Mojave to my alpine box running the fake servers.
Just a matter of compiling OpenSSL 1.0.2u, accepting the connection with the
right certificate, and proxying the raw TCP data. OpenSSL was linked statically
with the final binary, so we don’t break the whole OS.</p>

<p>The first request is a <code class="language-plaintext highlighter-rouge">GET</code> to <code class="language-plaintext highlighter-rouge">/rdr/pprdr.asp</code>. With some guesswork we can
assume <code class="language-plaintext highlighter-rouge">rdr</code> stands for <em>Redirect</em> or <em>Redirector</em>, and <code class="language-plaintext highlighter-rouge">pprdr</code> may stand for
<em>Passport Redirect(or)</em> – naming those things don’t really matter, but it’s nice
to give names to those pesky abbreviations Microsoft uses.</p>

<p>The response has no body, but the client expects an HTTP header named
<code class="language-plaintext highlighter-rouge">PassportURLs</code> with a few fields:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">DARealm</code>: The realm name. Not used by the client, but it requires it to be present.</li>
  <li><code class="language-plaintext highlighter-rouge">DALogin</code>: The realm host and login path. Microsoft seemed to use <code class="language-plaintext highlighter-rouge">nexus.passport.com/login2.srf</code>. The lack of schema
is intentional, the client will use HTTPS. If you are wondering, DA stands for
Domain Authentication.</li>
  <li><code class="language-plaintext highlighter-rouge">Properties</code>: Not used by the client, but it requires it to be present.</li>
</ul>

<p>Those pairs get concatenated as <code class="language-plaintext highlighter-rouge">K=V</code> separated by commas.</p>

<p>Then, the actual login magic happens. The client <code class="language-plaintext highlighter-rouge">POST</code>s to <code class="language-plaintext highlighter-rouge">DALogin</code> without a
body, but with an interesting header:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Authorization: Passport1.4 fake=1,OrgVerb=GET,OrgURL=http%3A%2F%2Fmessenger.msn.com,sign-in=hey%40vito.io,pwd=asdf
</code></pre></div></div>

<p>Breaking it down:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Passport1.4</code>: The authentication mechanism version. Hardcoded.</li>
  <li><code class="language-plaintext highlighter-rouge">fake=1</code>, the opaque value provided by the TCP server on the <code class="language-plaintext highlighter-rouge">S</code> phase of the
<code class="language-plaintext highlighter-rouge">TWN</code> operation. Everything connects!</li>
  <li><code class="language-plaintext highlighter-rouge">OrgVerb=GET</code>: I honestly have no idea what that’s supposed to be. Maybe
something for federation, or to allow Messenger servers outside Microsoft?</li>
  <li><code class="language-plaintext highlighter-rouge">OrgURL=http://messenger.msn.com</code>: Same as <code class="language-plaintext highlighter-rouge">OrgVerb</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">sign-in=hey@vito.io</code>: The identity of the user being authenticated</li>
  <li><code class="language-plaintext highlighter-rouge">pwd=asdf</code>: The password provided by the user on the login window.</li>
</ul>

<p>The response can again have an empty body, but the client expects a specific
header to be present:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Authentication-Info: Passport1.4 da-status=success,from-PP='a=HENLO'
</code></pre></div></div>

<p>Breaking it down:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">da-status</code>: The status returned from the Domain Authentication. Here we are
indicating the authentication was successful.</li>
  <li><code class="language-plaintext highlighter-rouge">from-PP='a=HENLO'</code>: Assuming this as <em>From Passport</em>. The value is opaque to
the client, it just returns this to the TCP server.</li>
</ul>

<p>This is the last interaction with Passport – for now! The dance continues with
the TCP server again, where the client sends the following message:</p>

<h3 id="back-to-tcp">Back to TCP</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>USR 4 TWN S a=HENLO 235411cb78234a468feb6f1bccca43fd
</code></pre></div></div>

<p>Same OP, different message ID – as it’s a new message, – and the value returned
from passport is handled opaquely and just returned to the TCP server. The final hex is 32 characters – MD5-length – but I haven’t confirmed what it represents. Correlation token, telemetry, session nonce; take your pick.</p>

<p>With this, the TCP server can just <em>FINALLY</em> complete the authentication flow
by replying with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>USR 4 OK hey@vito.io 1 0
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">OK</code>: Authentication passed. Hooray.</li>
  <li><code class="language-plaintext highlighter-rouge">hey@vito.io</code>: The user’s identity</li>
  <li><code class="language-plaintext highlighter-rouge">1</code>: Authentication flags. <code class="language-plaintext highlighter-rouge">1</code> seems to be enough to keep the client happy.</li>
  <li><code class="language-plaintext highlighter-rouge">0</code>: Capability flags. Just echoing from CVR.</li>
</ul>

<p>Should we expect something else over TCP now? Absolutely not. The plot thickens:</p>

<h2 id="microsoft-soap-services">Microsoft SOAP Services</h2>

<p>Microsoft loves SOAP for reasons one can’t even reason about. As I’m just trying
to boot the app, and not actively use it, the idea is to just provide the
minimal amount of stuff to make it get to the main window post-login.</p>

<p>Looking at how the client uses those endpoints, I can again just provide what
it deems required. One interesting thing to notice, however, is that for each
SOAP endpoint, the client seems to perform authentication once again against the
Passport service, and relays whatever the authentication endpoints provides in
response to a <code class="language-plaintext highlighter-rouge">TicketToken</code> field.</p>

<p>SOAP goes – at least initially – to one endpoint:</p>

<p><code class="language-plaintext highlighter-rouge">byrdr.omega.contacts.msn.com</code> receives a <code class="language-plaintext highlighter-rouge">POST</code> request for
<code class="language-plaintext highlighter-rouge">/abservice/SharingService.asmx</code>, with an HTTP header
<code class="language-plaintext highlighter-rouge">SOAPAction: http://www.msn.com/webservices/AddressBook/FindMembership</code>,
followed by another <code class="language-plaintext highlighter-rouge">POST</code>, this time to <code class="language-plaintext highlighter-rouge">/abservice/abservice.asmx</code>,
with the same header, but this time containing the value
<code class="language-plaintext highlighter-rouge">http://www.msn.com/webservices/AddressBook/ABFindAll</code>.</p>

<p>On those, <code class="language-plaintext highlighter-rouge">FindMembership</code> seems to be responsible to indicate the membership
graph. The MSN Address Book service (aka <code class="language-plaintext highlighter-rouge">abservice</code>) has several logical lists,
where each one corresponds to a different kind of “membership” in a materialised
view for the current user. Those lists are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">FL</code>: The <em>Forward List</em>, representing one’s contacts (people one added)</li>
  <li><code class="language-plaintext highlighter-rouge">AL</code>: The <em>Allow List</em>, representing who can see one’s presense status.</li>
  <li><code class="language-plaintext highlighter-rouge">BL</code>: The <em>Block List</em>, containing blocked contacts.</li>
  <li><code class="language-plaintext highlighter-rouge">RL</code>: The <em>Reverse List</em>, containing who added the user (reverse of <code class="language-plaintext highlighter-rouge">FL</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">PL</code>: The <em>Pending List</em>, containing pending invitations.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">FindMembership</code> provides the <code class="language-plaintext highlighter-rouge">AL</code>, <code class="language-plaintext highlighter-rouge">BL</code>, and <code class="language-plaintext highlighter-rouge">RL</code> lists, while <code class="language-plaintext highlighter-rouge">ABFindAll</code>
returns what’s enough for the client to rebuild the <code class="language-plaintext highlighter-rouge">FL</code> list.</p>

<p><code class="language-plaintext highlighter-rouge">ABFindAll</code> is a different story; it returns the full contact records for the
authenticated user. Each record contains several fields, including:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">contactId</code>: a GUID representing the contact.</li>
  <li><code class="language-plaintext highlighter-rouge">contactType</code>: the contact’s type. <code class="language-plaintext highlighter-rouge">Regular</code> is the only value that’s of my
interest right now.</li>
  <li><code class="language-plaintext highlighter-rouge">passportName</code>: the user’s Passport name. Basically their email.</li>
  <li><code class="language-plaintext highlighter-rouge">displayName</code> and <code class="language-plaintext highlighter-rouge">quickName</code>: the user’s display name. Not sure why it is
sent twice.</li>
</ul>

<p>The object may contain lots of other fields, but those are the most relevant
to achieve our objective.</p>

<p>Once those SOAPs are returned, the client resumes communication with the TCP
server:</p>

<h2 id="the-final-dance">The Final Dance</h2>

<p>The next operation requested by the client is a new kind we haven’t seen yet:
a two-part operation. This is what is sent:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADL 5 245\r\n
&lt;ml l="1"&gt;&lt;d n="local"&gt;&lt;c n="away" l="3" t="1" /&gt;&lt;c n="brb" l="3" t="1" /&gt;&lt;c n="busy" l="3" t="1" /&gt;&lt;c n="hidden" l="3" t="1" /&gt;&lt;c n="idle" l="3" t="1" /&gt;&lt;c n="lunch" l="3" t="1" /&gt;&lt;c n="online" l="3" t="1" /&gt;&lt;c n="phone" l="3" t="1" /&gt;&lt;/d&gt;&lt;/ml&gt;
</code></pre></div></div>

<p>This will need some explanation. Through the previous SOAP, I’m setting a few
users to have each one showing a different status. Here are the users:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  GUID                                 Passport
  ------------------------------------ -------------
  11111111-1111-1111-1111-111111111111 online@local
  22222222-2222-2222-2222-222222222222 busy@local
  33333333-3333-3333-3333-333333333333 idle@local
  44444444-4444-4444-4444-444444444444 away@local
  55555555-5555-5555-5555-555555555555 brb@local
  66666666-6666-6666-6666-666666666666 lunch@local
  77777777-7777-7777-7777-777777777777 phone@local
  88888888-8888-8888-8888-888888888888 hidden@local
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ADL</code> is the <em>Address List</em> command. The payload <code class="language-plaintext highlighter-rouge">245</code> indicates how many bytes
follow after the terminator (<code class="language-plaintext highlighter-rouge">\r\n</code>), comprising a second payload, that’s
formatted below:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ml</span> <span class="na">l=</span><span class="s">"1"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;d</span> <span class="na">n=</span><span class="s">"local"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"away"</span>   <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"brb"</span>    <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"busy"</span>   <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"hidden"</span> <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"idle"</span>   <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"lunch"</span>  <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"online"</span> <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;c</span> <span class="na">n=</span><span class="s">"phone"</span>  <span class="na">l=</span><span class="s">"3"</span> <span class="na">t=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/d&gt;</span>
<span class="nt">&lt;/ml&gt;</span>
</code></pre></div></div>

<p>The XML contains a single <code class="language-plaintext highlighter-rouge">ml</code> (<em>Membership List</em>) element with an <code class="language-plaintext highlighter-rouge">l</code> attribute
with value <code class="language-plaintext highlighter-rouge">1</code>, indicating this is the first list exchanged through the <code class="language-plaintext highlighter-rouge">ADL</code> OP.
Each sub-list is split by domain, and since all my users are <code class="language-plaintext highlighter-rouge">@local</code>, we get
a single <code class="language-plaintext highlighter-rouge">d</code> (<em>Domain</em>) element, with the <code class="language-plaintext highlighter-rouge">n</code> attribute set to <code class="language-plaintext highlighter-rouge">local</code>. Inside
we have each user as a <code class="language-plaintext highlighter-rouge">c</code> element, their name in the <code class="language-plaintext highlighter-rouge">n</code> attribute, <code class="language-plaintext highlighter-rouge">l</code> (<em>List</em>)
set to <code class="language-plaintext highlighter-rouge">3</code>, which is a bitmask indicating that the contact is both on <code class="language-plaintext highlighter-rouge">FL</code> and <code class="language-plaintext highlighter-rouge">AL</code>
lists and <code class="language-plaintext highlighter-rouge">t</code> (<em>Type</em>) set to <code class="language-plaintext highlighter-rouge">1</code>, indicating a Passport-type identity.</p>

<p>The bitmask values are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">1</code> = Forward List (<code class="language-plaintext highlighter-rouge">FL</code>) – contacts the user added</li>
  <li><code class="language-plaintext highlighter-rouge">2</code> = Allow List (<code class="language-plaintext highlighter-rouge">AL</code>) – contacts allowed to see the user’s status</li>
  <li><code class="language-plaintext highlighter-rouge">4</code> = Block List (<code class="language-plaintext highlighter-rouge">BL</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">8</code> = Reverse List (<code class="language-plaintext highlighter-rouge">RL</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">16</code> = Pending List (<code class="language-plaintext highlighter-rouge">PL</code>)</li>
</ul>

<p>For now we just acknowledge the message, so it can continue with its process:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADL 5 OK\r\n
</code></pre></div></div>

<p>The next OP we get from the client is <code class="language-plaintext highlighter-rouge">PRP</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PRP 6 MFN hey@vito.io
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">PRP</code> stands for <em>Profile Property</em>. The client is requesting a property to be
set: <code class="language-plaintext highlighter-rouge">MFN</code>, also known as <em>My Friendly Name</em>. Again, we just need to acknowledge
it, but this time with an <code class="language-plaintext highlighter-rouge">ACK</code> OP:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACK 6 PRP\r\n
</code></pre></div></div>

<p>The client continues with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BLP 7 AL
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">BLP</code> is the <em>Blocking Policy</em>. It controls the default privacy rule for people
not explicitly on <code class="language-plaintext highlighter-rouge">AL</code> and <code class="language-plaintext highlighter-rouge">BL</code>. The last token we get is the policy to be
applied, which can be one of:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">AL</code>: Allow by default. Unknown users can see and contact the current user.</li>
  <li><code class="language-plaintext highlighter-rouge">BL</code>: Block by default. Only allowed users can see and contact the current user.</li>
</ul>

<p>The client is setting the policy to <em>Allow</em>. Ok. We just acknowledge it again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACK 7 BLP\r\n
</code></pre></div></div>

<p>Next up: Status update. The client sends:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CHG 8 NLN 1085276192:131088
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CHG</code> requests a status update, <code class="language-plaintext highlighter-rouge">NLN</code> indicates <em>Online</em>, and is followed by
an optional capability bitmask (two numbers, colon-separated). For now I’ll
ignore those, but they will be important in the future, depending on how this
progresses.</p>

<p>For this one we will echo everything back to the client:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CHG 8 NLN 1085276192:131088\r\n
</code></pre></div></div>

<p>But this change is also an important trigger for the TCP server. From this point
onwards, the client accepts the <em>Initial Status List</em>, (<code class="language-plaintext highlighter-rouge">ILN</code>) which assigns
statuses and Display Names for each one of its contacts, followed by an <code class="language-plaintext highlighter-rouge">UBX</code>
OP that provides extra information regarding their profiles, such as a Personal
Status Message, and Media Status.</p>

<p>Here we reuse the Message ID from <code class="language-plaintext highlighter-rouge">CHG</code> to send one <code class="language-plaintext highlighter-rouge">ILN</code> for each contact,
along with an <code class="language-plaintext highlighter-rouge">UBX</code>. Notice, however, that <code class="language-plaintext highlighter-rouge">UBX</code> takes no Message ID. From
the previous contact list, I’ll also assign a random UUID for their PSMs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ILN 8 NLN online@local 1 Online%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX online@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;0b48cf1c-b9a5-4793-b8d4-a8ddd6b13c6c&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 BSY busy@local 1 Busy%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX busy@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;72b6dca4-811b-4c6a-8b2a-c87cfa6e3299&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 IDL idle@local 1 Idle%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX idle@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;88360ef7-5152-4ada-8bc8-e0343736e830&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 AWY away@local 1 Away%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX away@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;d430617a-17bf-4437-a8f2-54aa4d33444e&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 BRB brb@local 1 Brb%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX brb@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;ecd12daf-0c11-445f-b1ae-4751a509c25c&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 LUN lunch@local 1 Lunch%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX lunch@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;9bd609bc-d2f8-4e1a-be72-06804bf73183&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;

ILN 8 PHN phone@local 1 Phone%20User 1085276192 %3Cmsnobj%2F%3E\r\n
UBX phone@local 1 89\r\n
&lt;Data&gt;&lt;PSM&gt;b1482981-ad81-4a2a-872f-ba3331c6bb91&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">ILN</code> command has the following parameters after the Message ID:</p>

<ul>
  <li>The status (<code class="language-plaintext highlighter-rouge">NLN</code>, <code class="language-plaintext highlighter-rouge">BSY</code>, <code class="language-plaintext highlighter-rouge">IDL</code>, <code class="language-plaintext highlighter-rouge">AWY</code>, <code class="language-plaintext highlighter-rouge">BRB</code>, <code class="language-plaintext highlighter-rouge">LUN</code>, <code class="language-plaintext highlighter-rouge">PHN</code>)</li>
  <li>Their email address</li>
  <li>Their Network ID (<code class="language-plaintext highlighter-rouge">1</code> = Passport/Windows Live)</li>
  <li>Their display name, URL Encoded</li>
  <li>Their capabilities flag. Here I’m sending the same caps the client provided
to the server, except that it only takes the first portion (before the colon).</li>
  <li>A <code class="language-plaintext highlighter-rouge">msnobj</code> payload. Here it is empty, and has no significance to my objective.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">UBX</code> has the same format as <code class="language-plaintext highlighter-rouge">UUX</code> (see below).</p>

<blockquote>
  <p>As a side note, the required <code class="language-plaintext highlighter-rouge">msnobj</code> portion was what took most of the time
during this session. Without it the client just refused to assigned statuses
and failed silently. Codepaths leading to this portion were also quite
complicated, so getting to the point “oh, that’s what’s missing” took a while.</p>
</blockquote>

<p>Then, more state is sent by the client:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UUX 9 53
&lt;Data&gt;&lt;PSM&gt;&lt;/PSM&gt;&lt;CurrentMedia&gt;&lt;/CurrentMedia&gt;&lt;/Data&gt;
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">UUX</code> (<em>User eXtended</em> data) is a set of metadata announced by the client. Here
it’s setting the <code class="language-plaintext highlighter-rouge">PSM</code> (Personal Status Message), and the <code class="language-plaintext highlighter-rouge">CurrentMedia</code> to
empty values. Again, we just acknowledge it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACK 9 UUX\r\n
</code></pre></div></div>

<p>Following, we get two extra UUX messages:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UUX 10 75\r\n
&lt;EndpointData&gt;&lt;Capabilities&gt;1085276192:131088&lt;/Capabilities&gt;&lt;/EndpointData&gt;
</code></pre></div></div>

<p>and</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UUX 11 92\r\n
&lt;PrivateEndpointData&gt;&lt;EpName&gt;Rhodes&lt;/EpName&gt;&lt;ClientType&gt;1&lt;/ClientType&gt;&lt;/PrivateEndpointData&gt;
</code></pre></div></div>

<p>The first one contains the same capability set we got during the <code class="language-plaintext highlighter-rouge">CHG</code> OP. The
second is part of the “multi-endpoint” implementation that was being rolled,
allowing users to sign in in multiple places at the same time. It contains
the endpoint name <code class="language-plaintext highlighter-rouge">EpName</code>, that’s carrying the name of the computer running
Messenger (<code class="language-plaintext highlighter-rouge">Rhodes</code>), and a client type. We can assume <code class="language-plaintext highlighter-rouge">1</code> represents a Desktop.</p>

<p>Then, the client continues asking for a service URL using the <code class="language-plaintext highlighter-rouge">URL</code> OP:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>URL 12 MOBILE
</code></pre></div></div>

<p>There it is asking specifically for the Mobile Messaging / SMS Service MSN used
to support. For now, let’s just route it to our server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>URL 12 MOBILE http://10.0.10.126:8080/\r\n
</code></pre></div></div>

<p>And continues with further <code class="language-plaintext highlighter-rouge">URL</code> ops:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>URL 13 PROFILE 0x0409
URL 14 CHGMOB
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">PROFILE</code> is requesting the profile webpage/service URL for a LCID in hex,
again, <code class="language-plaintext highlighter-rouge">0x0409</code> is <code class="language-plaintext highlighter-rouge">en-US</code>. <code class="language-plaintext highlighter-rouge">CHGMOB</code> is the <em>Change Mobile</em> settings URL,
part of the <code class="language-plaintext highlighter-rouge">MOBILE</code> capability. For both we will reply the same URL as
<code class="language-plaintext highlighter-rouge">MOBILE</code>, as the client wouldn’t care anyway.</p>

<p>Followed by:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UUN 15 hey@vito.io 8 9
ignore me
</code></pre></div></div>

<p>I <em>think</em> UUN is <em>User Update/Notification</em> the value <code class="language-plaintext highlighter-rouge">8</code> is unknown, and <code class="language-plaintext highlighter-rouge">9</code>
represents the amount of bytes following. For some reason, the client is sending
<code class="language-plaintext highlighter-rouge">ignore me</code>. Weird. After some Trial-and-Error, the client is happy with a
simple ACK:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ACK 15 UUN\r\n
</code></pre></div></div>

<p>And finally, the client rests sending periodic <code class="language-plaintext highlighter-rouge">PNG</code> (<em>Ping</em>) OPs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PNG 16\r\n
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>QNG 60\r\n
</code></pre></div></div>

<p>The result is nothing less than satisfying.</p>

<p><img src="/assets/img/blog/messenger-on-mac/mm-macos.jpg" style="border-radius: 8px; width: 400px" /></p>

<p>And there it is. Eight contacts, eight statuses, no Microsoft infrastructure involved. A fake TCP server, a TLS shim, a handful of SOAP responses, and a lot of trial and error – enough to convince a 2008 Mac app it’s talking to servers that haven’t existed for over a decade.</p>

<p>That was just a little detour try to relax from the amount of Assembly and C/C++ on Windows. Did it work? No, I had to disassemble the macOS binary, but at least that is known territory.</p>

<p>Later on I will continue on the main project.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Just to take a breath between Assembly and graphics, I installed Mojave on an old Mac Mini and spun Microsoft Messenger, which is the equivalent of Windows Live Messenger for macOS, as removing Windows from the equation makes things way easier. The objective today is to fool it enough to make it think it is talking with the now long gone Microsoft servers.]]></summary></entry><entry><title type="html">Rebuilding Windows Live Messenger, Part 1: This is not RLE</title><link href="https://vito.io/articles/2026-02-15-this-is-not-rle.html" rel="alternate" type="text/html" title="Rebuilding Windows Live Messenger, Part 1: This is not RLE" /><published>2026-02-15T00:00:00-03:00</published><updated>2026-02-15T00:00:00-03:00</updated><id>https://vito.io/articles/this-is-not-rle</id><content type="html" xml:base="https://vito.io/articles/2026-02-15-this-is-not-rle.html"><![CDATA[<p>Some formats lie.</p>

<p>But this one… This one lied to me in the first three bytes.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>52 4C 45   →   "RLE"
</code></pre></div></div>

<p>It was not RLE.</p>

<p>Not byte-aligned runs, not <code class="language-plaintext highlighter-rouge">(count, value)</code>, not even something you could
generously call <em>RLE-like</em>. What sits behind that header is a bit-packed
predictive codec with interleaved channels, delta predictors, a meta-decoder
layered above colour decoding, and a rendering pipeline buried under more code
than anyone should have to read for a small icon.</p>

<p>What you are reading is about how I went from opaque binary blobs to a bit-exact
decoder, validated against the original renderer. No Microsoft code used. Only
observation, instrumentation, patience, and stubbornness.</p>

<p>This is the first step toward a side project I was thinking about for way too
long: recreating the Windows Live Messenger 2009 look-and-feel. Same visuals,
entirely new engine, open, secure, cross-platform. Not nostalgia, not
preservation. Reconstruction.</p>

<p>The most important part of this endeavour would be recreating the assets, or at
least extracting them. WLM2009 had stunning visuals that made it look great,
both on PCs and Macs. So the first task was set: Let’s get those assets. That
couldn’t be hard, right? <em>Right?</em></p>

<h2 id="where-are-the-assets">Where are the assets?</h2>

<p>The UI clearly contained intricate elements: icons, frames, gradients, control
visuals, but there were no images anywhere. One may expect to at least find
PNGs, BMPs, and such. But no PNG, no BMP, no JPEG, nothing that looked like
pixel data, nothing in the resources that resembled bitmaps. It was madness! How
could that amount of transparency and effects be nowhere?</p>

<p>Well, let’s make a small detour to a quick history lesson: Microsoft’s internal
UI frameworks.</p>

<p>Everything on WLM2009, and actually, in all Windows Live™ products of the same
era used an internal framework called DirectUI. After some research, it seems
that while people were suffering with MFC and WPF, Microsoft was using a custom
UI framework to make Windows Live, Office, and as far as I understood, parts of
Windows as well. That was DirectUI. People seem to think they gave up on that,
but I haven’t looked deeper, sorry.</p>

<p>So that meant that all that fancy stuff was being drawn by something else, but
as WLM also worked on Windows XP, Windows Vista, and Windows 7, binaries should
provide those facilities. Awesome, but where are the assets?</p>

<p>Interestingly, what those binaries lack in PNGs and other image formats, they
compensate with lots of blobs containing the same three-byte magic header:
<code class="language-plaintext highlighter-rouge">RLE</code> – Great! Everyone (okay, not <em>everyone</em>) knows RLE stands for Run-Length
Encoding, so decoding that would be easy: extract the RLE blob, strip the
header, run the algorithm, and we should get some kind of image.</p>

<p>No. Not this time. Doing this yielded nothing but garbage. Again, wrong
assumption. Now it feels like even Occam was also lying to me; a big conspiracy.</p>

<p>Jokes aside, one thing is clear: that was no RLE, but something else! Something
internal that only Microsoft knew, and that poked that small piece of my brain
that really, <em>really</em> likes to dig into this kind of thing – so it took the
wheel. Entropy analysis, heuristics, pattern matching, anything that could give
any kind of direction on what those blobs were about. Nothing.</p>

<p>That was the moment the situation became clear. These were not compressed
images. These could only be <em>the</em> images. There was no fallback, no original
bitmap stored somewhere else. The blob itself was the canonical representation
of the artwork. But if we cannot decode it, we cannot reproduce the UI. There is
no other source of truth.</p>

<h2 id="finding-the-real-decoder">Finding the real decoder</h2>

<p>Searching for <code class="language-plaintext highlighter-rouge">RLE</code> on the binaries leads to a forest of misleading symbols.
Helpers, wrappers, palette code, rendering glue, dead paths, UI plumbing. Most
of those never touch pixels. They prepare, route, or post-process, but they do
not decode.</p>

<p>Ghidra helped map the topology, but the generated C was often fiction. Registers
reused in unexpected ways confused the decompiler, calling conventions were
occasionally guessed wrong, stack frames collapsed, and control flow flattened
into something that looked plausible but was not what the CPU executed. The
assembly was the only reliable source of truth.</p>

<p>So the process became mechanical: Ghidra to understand structure, x32dbg to
observe reality, follow registers and observe the process’s memory, confirm
assumptions against execution, and treat decompiled C as a hint rather than
documentation; instead of trusting the C, the only source of truth was the
generated assembly from the binaries. And that hit hard.</p>

<p>But no problem, nothing we can’t solve, right? Eventually one function stood
out: <code class="language-plaintext highlighter-rouge">RLEDrawToDIB</code>. That’s the one responsible for converting the mysterious
<code class="language-plaintext highlighter-rouge">RLE</code> into something observable, and even better, on a DIB (Device-Independent
Bitmap), which can be exported.</p>

<p>Ok, we have the entrypoint, but what now?</p>

<h2 id="building-an-oracle">Building an Oracle</h2>

<p>The main executable helped a lot finding where and what was responsible for
drawing, but the amount of noise after that was just too much, after all, the
main binary did way more than just rendering. x32dbg was constantly pausing
execution at way too many places, and analysing the stack at those points
became tiring.</p>

<p>The plan now was to devise an oracle. Its job is to exercise only the hot path
concerning parsing and drawing. For that, one must first find the
entrypoint – which is already done, we have <code class="language-plaintext highlighter-rouge">RLEDrawToDIB</code>, – and make it work
isolated from the host binary. As it may be known, dear reader, I am no Windows
developer; most of my life I used Linux, Unix, OS X, and now macOS, so this is a
whole new world. But as I usually say, in the end it’s just code, so let’s jump
in and understand how to make this work.</p>

<p>The idea is dumb in the best way: load <code class="language-plaintext highlighter-rouge">uxcore.dll</code> into a process we control,
resolve internal functions by raw offset – no exports, no symbols, just hard
RVAs pulled from Ghidra. Reconstruct just enough of the calling sequence to
drive the real renderer into a DIB section we own. Then dump every pixel.</p>

<p>The shim is small: a macro computes function pointers as <code class="language-plaintext highlighter-rouge">base + offset</code> into
the loaded binary. A handful of <code class="language-plaintext highlighter-rouge">typedef</code>s describe the calling conventions
Ghidra helped identify, a thin wrapper mirrors the original dispatch
logic, including the nine-slice path, and routes execution through the same
code paths the real UI would take. The renderer writes into a top-down 32bpp
DIB section, BGRA in memory, which gets converted to RGBA and dumped as raw
bytes alongside a JSON sidecar with dimensions, pixel format, and colour-source
metadata.</p>

<p>That raw RGBA dump is the oracle. Not the PNG, as PNGs go through GDI+ encoding
and are useful for eyeballing, but byte-level comparison needs raw pixels. Every
single byte my clean-room decoder produces can now be diffed against what the
original renderer actually drew. No ambiguity, no interpretation, no room for
“close enough.”</p>

<p>But the oracle was not just a validation tool, but an exploration tool. With the
real renderer running under our control, we can instrument it, set breakpoints,
watch which functions get called and in what order, and trace the actual
execution path through those functions. Instead of staring at a forest of dead
code in Ghidra trying to guess what matters, we can <em>observe</em> what matters. The
oracle narrowed the search space from <em>everything</em> to <em>exactly this.</em></p>

<p>The process ran across 516 files exported from Messenger. For each one: load the
blob, ask <code class="language-plaintext highlighter-rouge">uxcore.dll</code> for the frame size, allocate a DIB, clear to transparent,
render, flush, dump. A system-colour table gets serialised at the end so
palette-mode files can be validated deterministically. The whole thing is a few
hundred lines of C++ and the ugliest <code class="language-plaintext highlighter-rouge">#define</code> you have ever seen, but it worked.</p>

<p>A note aside, outputting PNG using GDI+ is ugly as ugly gets, not to mention
Windows ABI. If there’s a hell, I’ve found it. I’m also skipping a whole part
where the oracle crashed, memory was corrupted, <code class="language-plaintext highlighter-rouge">NULL</code> pointers were
dereferenced, and everything that could go wrong, did. Just for context, here
what I tried once it refused to work:</p>

<ul>
  <li>Do we need to initialize COM for this process? Maybe. Would it hurt if we did
it? Hardly. So COM initialization was done. As the oracle was a “Console
Application”, in Windows lingo, <code class="language-plaintext highlighter-rouge">COINIT_APARTMENTTHREADED</code> seemed enough. That
didn’t change anything.</li>
  <li>Maybe Common Controls were missing? Did the DLL expect this to be ready?
<code class="language-plaintext highlighter-rouge">InitCommonControlsEx</code> to the rescue. Guess what? Nothing changed.</li>
  <li>Maybe GDI+ needed to be initialized? <code class="language-plaintext highlighter-rouge">GdiplusStartup</code> was called, and guess
what? That wasn’t it either.</li>
</ul>

<p>Only then I took the time to look at the host binary <em>again</em>, only to find two
interesting functions being called on <code class="language-plaintext highlighter-rouge">uxcore.dll</code>: <code class="language-plaintext highlighter-rouge">UXCoreInitProcess</code> and
<code class="language-plaintext highlighter-rouge">UXCoreInitThread</code>. And <em>ed ecco!</em> No crash.</p>

<p>This exposed the whole hot path the decoder used: every function could now be
isolated, inspected, and observed. “But how many?” you may be asking yourself,
and I’m glad you asked. From there the hot decoding path expanded into <em>one
hundred and twenty-one internal functions</em>, excluding Win32 calls. That was the
actual renderer. Now to get the gist of the format, we just need to extract all
of them as both C code and assembly. The result? ~20k SLOC of C code, ~41k of
Assembly. I hoped I had enough coffee in the kitchen for that and for the
blessing of Our Lord Turing, cause I knew that would hurt. Not that Assembly is
a problem, but after optimisations and some weird stuff we know Microsoft
compilers do, I knew I was signing up for headache and frustration.</p>

<h2 id="ghidra-vs-reality">Ghidra vs reality</h2>

<p>One may wonder why export both C and Assembly. Well, at several points, the
decompiler confidently produced code that looked reasonable and also was
completely wrong. And not wrong in subtle ways, but wrong in ways that
obliterate decoding on the first byte. Some examples (we will get to each of
these):</p>

<ul>
  <li><strong>Bit order:</strong> The decompiled logic strongly suggested MSB-first reading. The
stream is LSB-first. That single inversion produces garbage immediately, and it
is the kind of mistake that looks correct on paper until you watch actual
execution and realise every bit is landing in the wrong position.</li>
  <li><strong>The predictor:</strong> Decompiled output implied frequent resets. In reality, it
carries across runs within a row. That persistence is exactly why gradients
compress well, and why breaking it turns smooth ramps into staircases.</li>
  <li><strong>Row navigation:</strong> The decompiled view made it look absolute. It is
forward-relative, with row zero starting at the last index entry. Miss that,
and the decoder walks into the middle of someone else’s bitstream.</li>
  <li><strong>Alpha:</strong> The reconstructed C placed it inline with colour. In execution, it
sits <em>above</em> colour decoding, as a meta-layer that controls when colour tokens
are consumed. Without modelling that correctly, colour buffers desynchronise
and pixels drift. This one took the longest to untangle.</li>
  <li><strong>Token alignment:</strong> Loops in the pseudocode looked byte-aligned. They are
not. Everything is bit-aligned. Every. Single. Field.</li>
</ul>

<p>For those starting in reverse engineering: The lesson is simple, but important.
Decompiled code is not truth. Execution is.</p>

<h2 id="the-trampoline">The Trampoline</h2>

<p>The format remained opaque until a small piece of assembly changed everything.
There was a dispatch trampoline routing execution into different meta-decoders
depending on header flags. That explained why streams behaved differently even
when they looked similar.</p>

<p>Some used palette. Some used alpha runs. Some consumed colour continuously,
others stalled. Some interleaved channels, others did not. The structure finally
emerged. This was a bitstream, not a byte stream. Channels decoded
independently. Predictive delta replaced repetition. Alpha was not inline; it
existed in a meta-layer. Rows were not sequential but navigated via a pointer
table. Bits were read least-significant-bit first.</p>

<p>This was not RLE. This was a “small” image codec wearing an RLE badge.</p>

<h2 id="the-format">The Format</h2>

<p>With the oracle, the trampoline, and 41k lines of assembly in front of me, the
format started to take shape. What follows is a high-level overview, enough to
understand why calling this “RLE” is an insult to the codec. A full specification
with worked examples exists, but that deserves its own post.</p>

<h3 id="the-header">The Header</h3>

<p>Every blob opens with six bytes. Three magic bytes (<code class="language-plaintext highlighter-rouge">RLE</code>), an image count, a
flags byte encoding the tile topology and payload size width, and a DPI byte.
Six bytes. That is the entire file header.</p>

<p>The tile topology is a 4-bit nibble packed into the flags byte. Each bit
controls whether a fixed border exists on that edge — left, top, right, bottom.
The result is a nine-patch system: a 1x1 grid when no bits are set, a full 3x3
when all four are. Sixteen possible configurations, all encoded in half a byte.
After some research, I noticed Android also have those nine-patch PNGs! Same idea,
different decade. Microsoft got there first. (Is that a first?)</p>

<h3 id="sub-images">Sub-Images</h3>

<p>Each sub-image carries a 4-byte header dword packed with geometry and flags.
Width and height, yes, but also: palette mode, 5-bit or 8-bit precision, alpha
presence, and the width of the row-navigation index entries. Thirteen bits for
width, twelve for height, and the rest is flags. Every bit accounted for.</p>

<p>The row-navigation index is where things get even weirder. Rows are not stored
sequentially. A table of forward-relative offsets sits at the start of the pixel
data, and row zero begins at the position of the <em>last</em> index entry. It is
elegant once you see it, and completely opaque until you do.</p>

<h3 id="the-bit-cursor">The Bit-Cursor</h3>

<p>Everything past the row index is a bitstream. Not bytes. Bits. Read LSB-first
within each octet, shared across all channels in a row. The decoder maintains a
bit-cursor: a byte pointer and a bit offset, and every field, from token types
to run counts to delta widths, is read through it. Nothing is byte-aligned
unless it happens to be by coincidence.</p>

<h3 id="tokens">Tokens</h3>

<p>Each colour token starts with a 2-bit type and a <code class="language-plaintext highlighter-rouge">B</code>-bit run count, where <code class="language-plaintext highlighter-rouge">B</code> is
derived from the image width. We have four token types:</p>

<ul>
  <li><strong>Solid fill:</strong> one value, repeated.</li>
  <li><strong>Delta-positive:</strong> each pixel is the previous value plus a small positive offset.</li>
  <li><strong>Delta-negative:</strong> same, but subtracted.</li>
  <li><strong>Literal:</strong> raw values, one per pixel.</li>
</ul>

<p>The deltas are the reason this format compresses well. Gradients, anti-aliased
edges, smooth colour transitions: they all become tiny increments instead of
full pixel values. The predictor carries across tokens within a row, which is
what makes it work. And what makes it break if your decompiler tells you it
resets. I’m looking at you, Ghidra.</p>

<h3 id="the-alpha-meta-decoder">The Alpha Meta-Decoder</h3>

<p>And then there is alpha. It does not sit alongside colour: it wraps it.</p>

<p>A 2-bit alpha type is read before each group of pixels: transparent run, opaque
run, or explicit alpha. Transparent runs emit zeros and <em>skip</em> colour decoding
entirely. Opaque runs consume colour normally. Explicit alpha decodes an alpha
token first, then pulls the corresponding colour values from the channel
buffers.</p>

<p>This meta-layer is why the format was <em>so hard</em> to reverse. Colour and alpha
share the same bitstream, the same cursor, but alpha <em>controls</em> when colour is
consumed. Get that wrong and everything desynchronises. Ghidra placed it inline,
and guess what? <em>It is not inline.</em> It is a state machine sitting above the
channel interleaver.</p>

<h2 id="validation">Validation</h2>

<p>With the format understood and a clean-room decoder written in Python (no
dependencies beyond the standard library) it was time to answer the only
question that matters: does it match?</p>

<p>The oracle produced raw RGBA dumps for 516 files extracted from Messenger’s
binaries. The decoder ran against the same inputs. Byte-level comparison. No
thresholds, no visual inspection, no “close enough.”</p>

<p>516 files. 380 single-tile, 136 multi-tile nine-slice. 516 exact matches.
Not a single byte off.</p>

<h2 id="whats-next">What’s Next</h2>

<p>This post covered the archaeology: finding the format, building the tooling,
understanding the structure. The next one will go deeper: a full worked
decode of a real file, bit by bit, token by token, with annotated hex dumps
and the complete specification. If you enjoy reading binary formats the way
some people enjoy crossword puzzles, you will like it.</p>

<p>There is also the matter of the DPI scaling pipeline and the palette system,
both of which deserve their own attention. And eventually, the actual goal:
using all of this to reconstruct the Windows Live Messenger 2009 interface,
from scratch, on modern platforms.</p>

<p>But that is for another day.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Some formats lie.]]></summary></entry><entry><title type="html">Tickling a Sun Ray 2 - It Speaks!</title><link href="https://vito.io/articles/2025-12-15-tickling-sun-ray-it-speaks.html" rel="alternate" type="text/html" title="Tickling a Sun Ray 2 - It Speaks!" /><published>2025-12-15T00:00:00-03:00</published><updated>2025-12-15T00:00:00-03:00</updated><id>https://vito.io/articles/tickling-sun-ray-it-speaks</id><content type="html" xml:base="https://vito.io/articles/2025-12-15-tickling-sun-ray-it-speaks.html"><![CDATA[<p>This is the second post on a series I’m working on, regarding the black-box
reverse-engineering of a Sun Microsystems Ray 2 Desktop Unit. The first post
can be found here: <a href="/articles/2025-12-13-tickling-sun-ray-discovery">Tickling a Sun Ray 2 - DHCP Discovery</a>.</p>

<h2 id="new-findings">New Findings</h2>

<p>In the last post, I mentioned that I had omitted a few <code class="language-plaintext highlighter-rouge">DHCPACK</code> and <code class="language-plaintext highlighter-rouge">DHCPINFORM</code>
packets, assuming they weren’t particularly important. My DHCP knowledge is a
bit rusty, and I naïvely thought: <em>“DHCPINFORM? The client already has an IP. No big deal.”</em></p>

<p>That assumption was <em>completely</em> wrong.</p>

<p>What was actually happening is that the Ray was <em>requesting</em> someone to <em>inform</em>
it about some knobs so it could finish bootstrapping and start the boot
sequence! That should have been obvious, especially given that it was retrying
the same packet over and over since no one was answering it, but yes, I missed
that. Oops!</p>

<p>Now, back on track. The packet clearly indicated what it was looking for:</p>

<ul>
  <li>Option 53 (DHCP Message Type): DHCPINFORM (<code class="language-plaintext highlighter-rouge">0x08</code>)</li>
  <li>Option 55: Parameter Request List
    <ul>
      <li>Interface MTU (<code class="language-plaintext highlighter-rouge">0x1A</code>)</li>
      <li>X Window System Display Manager (<code class="language-plaintext highlighter-rouge">0x31</code>)</li>
      <li>TFTP Server Name (<code class="language-plaintext highlighter-rouge">0x42</code>)</li>
      <li>Vendor-Specific Information (<code class="language-plaintext highlighter-rouge">0x2B</code>)</li>
    </ul>
  </li>
</ul>

<p>The choices there are interesting indeed! MTU makes sense, but, XDM? <em>Odd at least.</em></p>

<p>At this point, we know that:</p>
<ul>
  <li>The Sun Ray successfully acquires an IP</li>
  <li>It explicitly requests vendor-specific parameters</li>
  <li>It stalls indefinitely without them</li>
</ul>

<p>Looking for some information on the Web I could find some piece of documentation
that Oracle haven’t burned yet, and oh my, it’s telling. For pure archival
purposes, I’m mirroring it here: <a href="/sun/ray2/Alternate-Client-Initialization-Reqs-Using-DHCP.html">Sun Ray Client Initialization Requirements Using DHCP</a>,
but what’s interesting there is the <em>Alternate Vendor-Specific DHCP Options</em>
section, listing exactly what we need. The response for the <code class="language-plaintext highlighter-rouge">DHCPINFORM</code> SHOULD
carry a bit of extra information in the <em>Vendor-Specific Information</em> TLV, but
the Ray could piggyback on “Standard” DHCP Options, if your DHCP server didn’t
support them. Clever, very Sun-ish.</p>

<p>What’s relevant for us is this: We could add a <em>Vendor-Specific Information</em> to
the <code class="language-plaintext highlighter-rouge">DHCPACK</code> of the <code class="language-plaintext highlighter-rouge">DHCPINFORM</code> packet, and the Ray would just use it. And we
also have A LOT of knobs that can be turned. Below is a quick summary:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">AuthSrvr</code>, Type: <code class="language-plaintext highlighter-rouge">IP</code> - Mandatory.  Single Sun Ray server IP addresses.</li>
  <li><code class="language-plaintext highlighter-rouge">AuthPort</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Sun Ray server port.</li>
  <li><code class="language-plaintext highlighter-rouge">NewTVer</code>, Type: <code class="language-plaintext highlighter-rouge">ASCII</code> - Desired firmware version.</li>
  <li><code class="language-plaintext highlighter-rouge">LogHost</code>, Type: <code class="language-plaintext highlighter-rouge">IP</code> - Syslog server IP address.</li>
  <li><code class="language-plaintext highlighter-rouge">LogKern</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Log level for kernel.</li>
  <li><code class="language-plaintext highlighter-rouge">LogNet</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Log level for network.</li>
  <li><code class="language-plaintext highlighter-rouge">LogUSB</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Log level for USB.</li>
  <li><code class="language-plaintext highlighter-rouge">LogVid</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Log level for video.</li>
  <li><code class="language-plaintext highlighter-rouge">LogAppl</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> -  Log level for firmware application.</li>
  <li><code class="language-plaintext highlighter-rouge">NewTBW</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Bandwidth cap, value is bits per second.</li>
  <li><code class="language-plaintext highlighter-rouge">FWSrvr</code>, Type: <code class="language-plaintext highlighter-rouge">IP</code> - Firmware TFTP server IP address.</li>
  <li><code class="language-plaintext highlighter-rouge">NewTDispIndx</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> -  Obsolete. Do not use.</li>
  <li><code class="language-plaintext highlighter-rouge">Intf</code>, Type: <code class="language-plaintext highlighter-rouge">ASCII</code> - Sun Ray server interface name</li>
  <li><code class="language-plaintext highlighter-rouge">NewTFlags</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Obsolete. Do not use.</li>
  <li><code class="language-plaintext highlighter-rouge">AltAuth</code>, Type: <code class="language-plaintext highlighter-rouge">IP</code> -  List of Sun Ray server IP addresses.</li>
  <li><code class="language-plaintext highlighter-rouge">BarrierLevel</code>, Type: <code class="language-plaintext highlighter-rouge">NUMBER</code> - Mandatory. Firmware Download: barrier level.</li>
</ul>

<p>Lots of interesting stuff, but let’s begin with what can really help us: Logging.
It seems the Ray is capable of emitting log data to a Syslog server, and we can
set the level for several facilities, including <em>firmware application</em>,
<em>network</em>, and <em>kernel</em>! That’s just awesome! Thanks, Sun!</p>

<p>Now, given it wants a Syslog server, one can assume that the log levels are also
standardised to the levels Syslog supports, and one would be absolutely right.
From <a href="https://datatracker.ietf.org/doc/html/rfc3164">RFC 3164 - The BSD syslog Protocol</a>,
page 9 contains <em>Table 2. syslog Message Severities</em>, listing from <code class="language-plaintext highlighter-rouge">0</code>
(Emergency) up to <code class="language-plaintext highlighter-rouge">7</code> (Debug). I think the box may use <code class="language-plaintext highlighter-rouge">6</code> (Informational) as
default, so setting it to <code class="language-plaintext highlighter-rouge">7</code> may make it be more chatty!</p>

<h3 id="encoding-vendor-specific-information">Encoding Vendor-Specific Information</h3>

<p>The archived page also explains with less-than-desired details the encoding
format, but it’s nothing more, nothing less than a plain old TLV structure.
Adorable!</p>

<p>The structure is composed of repeated TLVs: one byte for the tag, one byte for
the length, followed by <code class="language-plaintext highlighter-rouge">n</code> bytes of value. The only important thing to keep in
mind is that any <code class="language-plaintext highlighter-rouge">IP</code> type is a simple representation of the IP octets, meaning
that <code class="language-plaintext highlighter-rouge">192.168.100.1</code> becomes <code class="language-plaintext highlighter-rouge">0xC0</code>, <code class="language-plaintext highlighter-rouge">0xA8</code>, <code class="language-plaintext highlighter-rouge">0x64</code>, <code class="language-plaintext highlighter-rouge">0x01</code>.</p>

<p>Now let’s make the Ray talk to us through syslog, and make it chatty. The DHCP
Option payload for <em>Vendor-Specific Information</em> would then be:</p>

<ul>
  <li>Option <code class="language-plaintext highlighter-rouge">0x2B</code>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">0x18</code>, <code class="language-plaintext highlighter-rouge">0x04</code>, <code class="language-plaintext highlighter-rouge">0xC0</code>, <code class="language-plaintext highlighter-rouge">0xA8</code>, <code class="language-plaintext highlighter-rouge">0x64</code>, <code class="language-plaintext highlighter-rouge">0x01</code>. (<code class="language-plaintext highlighter-rouge">LogHost</code>, 4 bytes, <code class="language-plaintext highlighter-rouge">192.168.100.1</code>)</li>
      <li><code class="language-plaintext highlighter-rouge">0x25</code>, <code class="language-plaintext highlighter-rouge">0x01</code>, <code class="language-plaintext highlighter-rouge">0x07</code>. (<code class="language-plaintext highlighter-rouge">LogKern</code>, 1 byte, value 7)</li>
      <li><code class="language-plaintext highlighter-rouge">0x26</code>, <code class="language-plaintext highlighter-rouge">0x01</code>, <code class="language-plaintext highlighter-rouge">0x07</code>. (<code class="language-plaintext highlighter-rouge">LogNet</code>, 1 byte, value 7)</li>
      <li><code class="language-plaintext highlighter-rouge">0x27</code>, <code class="language-plaintext highlighter-rouge">0x01</code>, <code class="language-plaintext highlighter-rouge">0x07</code>. (<code class="language-plaintext highlighter-rouge">LogUSB</code>, 1 byte, value 7)</li>
      <li><code class="language-plaintext highlighter-rouge">0x28</code>, <code class="language-plaintext highlighter-rouge">0x01</code>, <code class="language-plaintext highlighter-rouge">0x07</code>. (<code class="language-plaintext highlighter-rouge">LogVid</code>, 1 byte, value 7)</li>
      <li><code class="language-plaintext highlighter-rouge">0x29</code>, <code class="language-plaintext highlighter-rouge">0x01</code>, <code class="language-plaintext highlighter-rouge">0x07</code>. (<code class="language-plaintext highlighter-rouge">LogAppl</code>, 1 byte, value 7)</li>
    </ul>
  </li>
</ul>

<p>I don’t want to bring up a full syslog daemon, so listening to UDP port <code class="language-plaintext highlighter-rouge">514</code>
is enough.</p>

<p>And surely, we got the little box to talk with us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Network: soff 0 n 1
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Network: GetNextAltAuth 0 0 0
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Application: Stop tokens 8009df20
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Application: sessionMonitor TcpOpen timeout to 0.0.0.0:7009
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Network: soff 0 n 1
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x48b 0:14:4f:85:f2:b Network: GetNextAltAuth 0 0 0
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x873 0:14:4f:85:f2:b Application: getNextServer: no message - timeout
</code></pre></div></div>

<p>Gorgeous! It can talk, and the log levels do map cleanly to syslog severities.
That <code class="language-plaintext highlighter-rouge">&lt;15&gt;</code> is a PRI value in the syslog protocol, obtained by
<code class="language-plaintext highlighter-rouge">(facility * 8) + severity</code>, as defined by the spec. Working backwards:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>15 / 8 = 1 remainder 7
</code></pre></div></div>

<p>Meaning:</p>

<ul>
  <li>Facility: 1 (aka user-level messages)</li>
  <li>Severity: 7 (aka Debug)</li>
</ul>

<h3 id="the-authentication-server">The Authentication Server</h3>

<p>It seems to be trying to do something, but failing. That’s expected, since the
essential value is still missing from the Vendor TLV: <code class="language-plaintext highlighter-rouge">AuthSrvr</code>. It’s even
marked as <em>mandatory</em>! One funny detail about that table is that, although the
<code class="language-plaintext highlighter-rouge">BarrierLevel</code> is also marked as mandatory, the client continues the process
normally. More on that later.</p>

<p>Now let’s set the <code class="language-plaintext highlighter-rouge">AuthSrvr</code> value. It’s the same as <code class="language-plaintext highlighter-rouge">LogHost</code>, but with a
different tag:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">0x15</code>, <code class="language-plaintext highlighter-rouge">0x04</code>, <code class="language-plaintext highlighter-rouge">0xC0</code>, <code class="language-plaintext highlighter-rouge">0xA8</code>, <code class="language-plaintext highlighter-rouge">0x64</code>, <code class="language-plaintext highlighter-rouge">0x01</code>. (<code class="language-plaintext highlighter-rouge">AuthSrv</code>, 4 bytes, <code class="language-plaintext highlighter-rouge">192.168.100.1</code>)</li>
</ul>

<p>After updating what’s mimicking the DHCP server the Ray expects and adding the
potentially last flag it wants, we have new stuff on the syslog!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1e9 0:14:4f:85:f2:b Network: GetNextAltAuth 0 c0a86401 0
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1e9 0:14:4f:85:f2:b Application: Stop tokens 8009df20
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1e9 0:14:4f:85:f2:b Application: sessionMonitor TcpOpen timeout to 192.168.100.1:7009
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1e9 0:14:4f:85:f2:b Network: GetNextAltAuth 0 c0a86401 0
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1fd 0:14:4f:85:f2:b Network: GetNewtBandwidth: status down speed 100000000 mode full
</code></pre></div></div>

<p>There’s one little thing that may catch your eye: <code class="language-plaintext highlighter-rouge">Application: sessionMonitor TcpOpen timeout to 192.168.100.1:7009</code>.
<code class="language-plaintext highlighter-rouge">TCPOpen</code>? To the IP of the <code class="language-plaintext highlighter-rouge">AuthSrvr</code>? Port <code class="language-plaintext highlighter-rouge">7009</code>? That’s oddly familiar! We
had a mysterious UDP packet going to that port in the last post!</p>

<h4 id="the-mysterious-serverq">The Mysterious <code class="language-plaintext highlighter-rouge">serverQ</code></h4>

<p>To be honest, <code class="language-plaintext highlighter-rouge">serverQ</code> was a dead-end. I couldn’t find any references on what
it means, how it should respond, and why that happens. But hey! Now we have the
Ray trying to open a full-fledged TCP connection with us! I mean, it’s <em>trying</em>,
and that’s progress!
I’m calling this out explicitly because the previous post ended on a bit of a
cliffhanger, and I hate those as much as you probably do. For now, <code class="language-plaintext highlighter-rouge">serverQ</code>
remains unexplained, but it no longer blocks progress.</p>

<p>Moving on!</p>

<h3 id="a-sun-ray-walks-into-a-bar">A Sun Ray walks into a bar…</h3>

<p>Since the Ray <em>wants</em> to speak, let it speak! Let’s spin a TCP server listening
on port <code class="language-plaintext highlighter-rouge">7009</code> and dump whatever we get there. Here’s a few bytes of output from
the hacky TCP server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DummyAuthSrvr: 696e666f526571204d54553d31353...
</code></pre></div></div>

<p>For the well-trained eye, that hex looks an awful lot like an ASCII blob. And
guess what? It is! Here’s the ASCII version of it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>infoReq MTU=1500 _=1 barrierLevel=310 cause=insert clientRand=nrlifRNCnn4xe.F5Be/DA7gkwFF9cOXQOmxxzu8SM3e ddcconfig=1 event=insert firstServer=c0a86401 fw=MfgPkg_4.15_2006.07.20.16.57 hw=SunRayP8 id=00144f85f20b initState=1 namespace=IEEE802 pn=53622 realIP=c0a86432 sn=00144f85f20b startRes=1400x1050:1400x1050 state=disconnected tokenSeq=1 type=pseudo
</code></pre></div></div>

<p>And that’s A LOT! Holy guacamole!</p>

<p>Let’s try to break it up:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">infoReq</code>: Possibly an operation identifier?</li>
  <li><code class="language-plaintext highlighter-rouge">MTU</code>: The MTU we already provided to it.</li>
  <li><code class="language-plaintext highlighter-rouge">_</code>: No idea. Maybe a placeholder, maybe something deprecated. Who knows!</li>
  <li><code class="language-plaintext highlighter-rouge">barrierLevel</code>: Possibly the <code class="language-plaintext highlighter-rouge">BarrierLevel</code> we didn’t provide it. Something
related to firmware download, but let’s leave that for another time.</li>
  <li><code class="language-plaintext highlighter-rouge">cause</code>: Perhaps what made the Ray request that operation?</li>
  <li><code class="language-plaintext highlighter-rouge">clientRand</code>: A Base64! Sweet! Given the entropy, it’s really random! Perhaps
for some kind of negotiation? I think we will eventually figure that out.</li>
  <li><code class="language-plaintext highlighter-rouge">ddcconfig</code>: No idea.</li>
  <li><code class="language-plaintext highlighter-rouge">event</code>: The same value of <code class="language-plaintext highlighter-rouge">cause</code>? We will need some other packages to figure
this one out as well.</li>
  <li><code class="language-plaintext highlighter-rouge">firstServer</code>: That’s the server’s IP in hexadecimal. Nothing interesting so
far.</li>
  <li><code class="language-plaintext highlighter-rouge">fw</code>: A firmware version! Seems like this one is from 2006. Cute!</li>
  <li><code class="language-plaintext highlighter-rouge">hw</code>: A hardware identifier! It is a <code class="language-plaintext highlighter-rouge">SunRayP8</code>. Duly noted.</li>
  <li><code class="language-plaintext highlighter-rouge">id</code>: I don’t know, but this value is static across all messages it sent. (And resent.)</li>
  <li><code class="language-plaintext highlighter-rouge">initState</code>: Not a clue!</li>
  <li><code class="language-plaintext highlighter-rouge">namespace</code>: <code class="language-plaintext highlighter-rouge">IEEE802</code> is a fancy name for Ethernet. Maybe this thing (or other revisions)
could support other connection methods?</li>
  <li><code class="language-plaintext highlighter-rouge">pn</code>: This value changes in a non-monotonic fashion. Packet number? We will figure out.</li>
  <li><code class="language-plaintext highlighter-rouge">realIP</code>: This is the Ray’s IP in hexadecimal.</li>
  <li><code class="language-plaintext highlighter-rouge">sn</code>: Also static across all packets. Possibly a serial number.</li>
  <li><code class="language-plaintext highlighter-rouge">startRes</code>: It’s initial resolution?</li>
  <li><code class="language-plaintext highlighter-rouge">state</code>: Disconnected? Maybe because authentication hasn’t completed?</li>
  <li><code class="language-plaintext highlighter-rouge">tokenSeq</code>: I’m sure we will eventually figure out as well.</li>
  <li><code class="language-plaintext highlighter-rouge">type</code>: <em>pseudo</em> is an interesting choice. I’m interested, but not a clue.</li>
</ul>

<p>Then, it immediately sends another packet:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DummyAuthSrvr: 6b656570416c697665526571205f3...
</code></pre></div></div>

<p>Again, more ASCII:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>keepAliveReq _=1 fw=MfgPkg_4.15_2006.07.20.16.57 hw=SunRayP8 namespace=IEEE802 pn=53622 sn=00144f85f20b state=disconnected
</code></pre></div></div>

<p>And we have more details now!</p>

<ol>
  <li>Seems like we have another operation: <code class="language-plaintext highlighter-rouge">keepAliveReq</code></li>
  <li><code class="language-plaintext highlighter-rouge">pn</code> is the same across both messages. Possibly not a packet number!</li>
  <li>Everything else stays the same.</li>
</ol>

<p>Since we’re not answering after that point it just eventually times out and
retries:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x16ca1 0:14:4f:85:f2:b Network: TcpReceive: FIN received 805bdd78 1
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x16ca1 0:14:4f:85:f2:b Application: restartAuth
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x16ca1 0:14:4f:85:f2:b Network: TcpClose: 805bdd78
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x16ca1 0:14:4f:85:f2:b Application: TcpClose1 failed
[syslog from 192.168.100.50:514] &lt;14&gt; 0x0.0x16ca1 0:14:4f:85:f2:b Network: FreeTcb:Freeing Tcb before the connection is closed
</code></pre></div></div>

<p>It’s not rebooting, though. Only restarting the authentication phase. It may be
worth noticing that whenever it retries, even without rebooting both <code class="language-plaintext highlighter-rouge">clientRand</code>
and <code class="language-plaintext highlighter-rouge">pn</code> changes, but everything else stays the same. There’s one small detail,
though! Here’s a new detail in syslog:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[syslog from 192.168.100.50:514] &lt;13&gt; 0x0.0x544 0:14:4f:85:f2:b USB: rdd: unable to contact device manager on 192.168.100.1:7011  retrying
</code></pre></div></div>

<p>It’s ALSO trying to establish a connection to port 7011! Looking at my packet
dumps, it tried to open a TCP connection to that port. Let’s also see what
happens if we let it! It seems to be something called a <em>Device Manager</em>, so
let’s do the same we did with the <code class="language-plaintext highlighter-rouge">AuthSrvr</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DummyDeviceManager: 636f6e6e6563740a
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1f270 0:14:4f:85:f2:b Network: TcpClose: 8059c538
[syslog from 192.168.100.50:514] &lt;15&gt; 0x0.0x1f270 0:14:4f:85:f2:b Network: TcpReceive: FIN received 8059c538 8
[syslog from 192.168.100.50:514] &lt;14&gt; 0x0.0x1f2d8 0:14:4f:85:f2:b Network: TCBDaemon: packet receive error
[syslog from 192.168.100.50:514] &lt;14&gt; 0x0.0x1f33c 0:14:4f:85:f2:b Network: TCBDaemon: packet receive error
[syslog from 192.168.100.50:514] &lt;14&gt; 0x0.0x1f3a0 0:14:4f:85:f2:b Network: TCBDaemon: packet receive error
...
</code></pre></div></div>

<p>Yeah, it’s not happy. Again, an ASCII payload. This time it only says <code class="language-plaintext highlighter-rouge">connect</code>,
followed by a newline.</p>

<p>Eventually it will close the connection and send another <code class="language-plaintext highlighter-rouge">connect</code>, while the
<code class="language-plaintext highlighter-rouge">AuthSrvr</code> continues receiving <code class="language-plaintext highlighter-rouge">keepAliveReq</code>.</p>

<p>For now, that’s it. I’ll eventually rewrite the C stuff in Golang just for the
sake of it, and keep it updated in a Git repository. This way we can isolate the
<code class="language-plaintext highlighter-rouge">AuthSrvr</code>, <code class="language-plaintext highlighter-rouge">Device Manager</code>, and the hacky DHCP server in a single place.</p>

<p>I said I hate cliffhangers, and I meant it, but unfortunately the Sun Ray just
gave me a really good one.</p>

<p>In the next post, we’ll respond to its requests and see what happens when we get
it wrong.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This is the second post on a series I’m working on, regarding the black-box reverse-engineering of a Sun Microsystems Ray 2 Desktop Unit. The first post can be found here: Tickling a Sun Ray 2 - DHCP Discovery.]]></summary></entry><entry><title type="html">Tickling a Sun Ray 2 - DHCP Discovery</title><link href="https://vito.io/articles/2025-12-13-tickling-sun-ray-discovery.html" rel="alternate" type="text/html" title="Tickling a Sun Ray 2 - DHCP Discovery" /><published>2025-12-13T00:00:00-03:00</published><updated>2025-12-13T00:00:00-03:00</updated><id>https://vito.io/articles/tickling-sun-ray-discovery</id><content type="html" xml:base="https://vito.io/articles/2025-12-13-tickling-sun-ray-discovery.html"><![CDATA[<p>This Christmas I just got a brand new, still sealed, Sun Ray 2.
Basically the best gift I ever got. Period.</p>

<p>The Ray is a thin client that connects to a remote Sun server. I already ordered
a Sun Fire, but until it arrives, I decided to poke the little thing and see what
makes it tick!</p>

<p>After powering on, and having a network cable connected, the Ray shows a
little screen with its MAC address, the network cable speed, and if it’s reached
a local server. Since the Sun Fire is not here, the last part just does not
happen, so let’s begin by observing how it tries to reach the server.</p>

<h2 id="dhcpdiscover">DHCPDISCOVER</h2>

<p>The Ray starts with a <code class="language-plaintext highlighter-rouge">DHCPDISCOVER</code> to try to reach its server, which is no big
surprise:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0000   ff ff ff ff ff ff 00 14 4f 85 f2 0b 08 00 45 00   ........O.....E.
0010   01 48 00 28 40 00 ff 11 7a 7d 00 00 00 00 ff ff   .H.(@...z}......
0020   ff ff 00 44 00 43 01 34 00 00 01 01 06 00 ff 4e   ...D.C.4.......N
0030   c3 a7 04 7e 00 00 00 00 00 00 00 00 00 00 00 00   ...~............
0040   00 00 00 00 00 00 00 14 4f 85 f2 0b 00 00 00 00   ........O.......
0050   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0060   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0070   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0080   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0090   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00a0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00b0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00c0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00d0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00e0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00f0   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0100   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0110   00 00 00 00 00 00 63 82 53 63 35 01 01 3c 0e 53   ......c.Sc5..&lt;.S
0120   55 4e 57 2e 4e 65 77 54 2e 53 55 4e 57 3d 07 01   UNW.NewT.SUNW=..
0130   00 14 4f 85 f2 0b ff 00 00 00 00 00 00 00 00 00   ..O.............
0140   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0150   00 00 00 00 00 00                                 ......
</code></pre></div></div>

<h3 id="dix--ethernet-ii">DIX / Ethernet II</h3>

<p>The first portion is a classic DIX Ethernet frame (or Ethernet II, if you’re
still litigating DIX credit). From <code class="language-plaintext highlighter-rouge">0x00</code> to <code class="language-plaintext highlighter-rouge">0x05</code> we have the broadcast
address <code class="language-plaintext highlighter-rouge">ff:ff:ff:ff:ff:ff</code>, the second one is the Ray’s address,
<code class="language-plaintext highlighter-rouge">00:14:4f:85:f2:0b</code>, <code class="language-plaintext highlighter-rouge">0x0C</code> to <code class="language-plaintext highlighter-rouge">0x0D</code> indicates an IPv4 packet (<code class="language-plaintext highlighter-rouge">0x0800</code>).</p>

<h3 id="ipv4">IPv4</h3>

<p>The second portion, as expected, is the IPv4 header. Nothing new there as well,
the only relevant information there is that the destination address is
<code class="language-plaintext highlighter-rouge">255.255.255.255</code>, and it’s a UDP packet; this basically covers bytes <code class="language-plaintext highlighter-rouge">0x0E</code> to
<code class="language-plaintext highlighter-rouge">0x21</code>.</p>

<h3 id="udp">UDP</h3>

<p>From <code class="language-plaintext highlighter-rouge">0x22</code> to <code class="language-plaintext highlighter-rouge">0x29</code> we can observe the UDP header. Interesting points are
source port 68, and destination port 67.</p>

<h3 id="bootpdhcp-fixed-header--dhcp-options">BOOTP/DHCP fixed header + DHCP options</h3>

<p>From <code class="language-plaintext highlighter-rouge">0x2A</code> onwards we have the <code class="language-plaintext highlighter-rouge">DHCPDISCOVER</code> payload. Finally something
interesting:</p>

<ul>
  <li>Message Type is <code class="language-plaintext highlighter-rouge">0x01</code>, meaning it’s a Boot Request,</li>
  <li>Hardware Type is <code class="language-plaintext highlighter-rouge">0x01</code>, or “Ethernet”</li>
  <li>Hardware Address Length is <code class="language-plaintext highlighter-rouge">0x06</code></li>
  <li>Hops is set as <code class="language-plaintext highlighter-rouge">0x00</code></li>
  <li>There’s a Transaction ID (<code class="language-plaintext highlighter-rouge">0xff4ec3a7</code>)</li>
  <li>A “Seconds Elapsed” field, from the BOOTP era (which the Ray is actively using!).
This is an interesting find: nowadays many implementations don’t do much with
this field, and plenty of clients just leave it at zero, but this was important
back then for a few different reasons! This field aided:
    <ul>
      <li>Relay agents decide whether to forward a request</li>
      <li>Servers decide whether to prioritize a client that has been waiting longer</li>
      <li>Debugging slow or failing DHCP negotiations</li>
    </ul>
  </li>
  <li>BOOTP flags all zeroed; basically indicating the client is capable of
receiving unicast replies</li>
  <li>Zeroed IP addresses</li>
  <li>The Ray’s MAC address, again</li>
  <li>A lot of padding for the Ray’s hardware address</li>
  <li>An empty server hostname</li>
  <li>An empty boot file name</li>
  <li>The Magic Cookie</li>
  <li>And FINALLY what I’m looking for:
    <ul>
      <li>Option 53, indicating the discovery packet</li>
      <li>Option 60, containing the vendor class identifier
        <ul>
          <li>The value is <code class="language-plaintext highlighter-rouge">SUNW.NewT.SUNW</code></li>
        </ul>
      </li>
      <li>Option 61 identifies the client using a hardware-type-prefixed identifier
  (Ethernet + MAC address), effectively restating the Ray’s MAC in a
  DHCP-standardized form. Again.</li>
      <li>Option 255, indicating no more options follow</li>
    </ul>
  </li>
  <li>And a lot of padding.</li>
</ul>

<p>So what’s interesting so far?</p>

<p>The big oddity is what’s missing: Option 55 (Parameter Request List). Most DHCP
clients use it to ask for things like DNS servers, routes, NTP, etc. The Sun Ray
doesn’t ask, it just identifies itself (Vendor Class + Client ID) and waits to
see what comes back. That suggests this client either runs with strong defaults,
or expects a Sun-aware environment to “just know” what to provide.</p>

<p>Also: <code class="language-plaintext highlighter-rouge">SUNW.NewT.SUNW</code> looks like a Sun vendor-class marker; possibly a
<em>New Terminal</em> generation marker rather than a generic Sun Ray family
identifier.</p>

<p>This is the first part of the whole negotiation, and lots can already be observed
and speculated!</p>

<p>Now, let’s make something that replies the Ray as it expects. I could just
leverage dnsmasq for that, but let’s go the fun way(tm) and write a bit of C:</p>

<details>
  <summary>
    <p><code class="language-plaintext highlighter-rouge">ray_dhcp.c</code></p>
  </summary>

  <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//</span>
<span class="c1">// Created by vitosartori on 12/13/25.</span>
<span class="c1">//</span>

<span class="c1">// Minimal DHCP responder (DISCOVER-&gt;OFFER, REQUEST-&gt;ACK) for RE labs.</span>
<span class="c1">// Build:  gcc -O2 -Wall -Wextra -o ray_dhcp ray_dhcp.c</span>
<span class="c1">// Run:    sudo ./ray_dhcp enp2s0 192.168.100.1 192.168.100.50 255.255.255.0 192.168.100.1</span>

<span class="cp">#define _GNU_SOURCE
#include</span> <span class="cpf">&lt;arpa/inet.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;errno.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;netinet/in.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/socket.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/types.h&gt;</span><span class="cp">
</span>
<span class="k">enum</span> <span class="p">{</span>
    <span class="n">BOOTREQUEST</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">BOOTREPLY</span>   <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>

    <span class="n">HTYPE_ETHERNET</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">HLEN_ETHERNET</span>  <span class="o">=</span> <span class="mi">6</span><span class="p">,</span>

    <span class="n">DHCP_COOKIE</span> <span class="o">=</span> <span class="mh">0x63825363</span><span class="p">,</span>

    <span class="c1">// DHCP Options</span>

    <span class="n">OPT_PAD</span>        <span class="o">=</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">OPT_SUBNETMASK</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">OPT_ROUTER</span>     <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">OPT_REQ_IP</span>     <span class="o">=</span> <span class="mi">50</span><span class="p">,</span>
    <span class="n">OPT_LEASETIME</span>  <span class="o">=</span> <span class="mi">51</span><span class="p">,</span>
    <span class="n">OPT_MSGTYPE</span>    <span class="o">=</span> <span class="mi">53</span><span class="p">,</span>
    <span class="n">OPT_SERVERID</span>   <span class="o">=</span> <span class="mi">54</span><span class="p">,</span>
    <span class="n">OPT_VENDORCLASS</span><span class="o">=</span> <span class="mi">60</span><span class="p">,</span>
    <span class="n">OPT_CLIENTID</span>   <span class="o">=</span> <span class="mi">61</span><span class="p">,</span>
    <span class="n">OPT_END</span>        <span class="o">=</span> <span class="mi">255</span><span class="p">,</span>

    <span class="c1">// DHCP Message Types</span>

    <span class="n">DHCPDISCOVER</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">DHCPOFFER</span>    <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
    <span class="n">DHCPREQUEST</span>  <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">DHCPACK</span>      <span class="o">=</span> <span class="mi">5</span>
<span class="p">};</span>

<span class="cp">#pragma pack(push, 1)
</span><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">uint8_t</span>  <span class="n">op</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">htype</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">hlen</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">hops</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">xid</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">secs</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">flags</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">ciaddr</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">yiaddr</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">siaddr</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">giaddr</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">chaddr</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
    <span class="kt">uint8_t</span>  <span class="n">sname</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span>
    <span class="kt">uint8_t</span>  <span class="n">file</span><span class="p">[</span><span class="mi">128</span><span class="p">];</span>
    <span class="c1">// Following is cookie + TLVs</span>
<span class="p">}</span> <span class="n">bootp_t</span><span class="p">;</span>
<span class="cp">#pragma pack(pop)
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">uint8_t</span>  <span class="n">msgtype</span><span class="p">;</span>       <span class="c1">// Option 53</span>
    <span class="kt">uint8_t</span>  <span class="n">has_msgtype</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">vendor</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span>   <span class="c1">// Option 60</span>
    <span class="kt">uint8_t</span>  <span class="n">vendor_len</span><span class="p">;</span>
    <span class="kt">uint8_t</span>  <span class="n">has_vendor</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">req_ip</span><span class="p">;</span>        <span class="c1">// Option 50 (optional)</span>
    <span class="kt">uint8_t</span>  <span class="n">has_req_ip</span><span class="p">;</span>
<span class="p">}</span> <span class="n">dhcp_opts_t</span><span class="p">;</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">die</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">msg</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">perror</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">parse_ipv4</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">s</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">out_nbo</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">in_addr</span> <span class="n">a</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">inet_pton</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">s</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">a</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="o">*</span><span class="n">out_nbo</span> <span class="o">=</span> <span class="n">a</span><span class="p">.</span><span class="n">s_addr</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">print_mac</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">mac</span><span class="p">[</span><span class="mi">6</span><span class="p">],</span> <span class="kt">char</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="k">const</span> <span class="kt">size_t</span> <span class="n">buf_len</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">snprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">buf_len</span><span class="p">,</span> <span class="s">"%02x:%02x:%02x:%02x:%02x:%02x"</span><span class="p">,</span>
             <span class="n">mac</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">mac</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">mac</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">mac</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="n">mac</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span> <span class="n">mac</span><span class="p">[</span><span class="mi">5</span><span class="p">]);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">parse_dhcp_options</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">p</span><span class="p">,</span> <span class="k">const</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">,</span> <span class="n">dhcp_opts_t</span> <span class="o">*</span><span class="n">o</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">o</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">o</span><span class="p">));</span>

    <span class="c1">// First the cookie</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">cookie</span><span class="p">;</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cookie</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ntohl</span><span class="p">(</span><span class="n">cookie</span><span class="p">)</span> <span class="o">!=</span> <span class="n">DHCP_COOKIE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"drop: cookie mismatch (got %08x)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ntohl</span><span class="p">(</span><span class="n">cookie</span><span class="p">));</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>

    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">code</span> <span class="o">=</span> <span class="n">p</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="n">OPT_PAD</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="n">OPT_END</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">&gt;=</span> <span class="n">n</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>

        <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">len</span> <span class="o">=</span> <span class="n">p</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="n">len</span> <span class="o">&gt;</span> <span class="n">n</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">val</span> <span class="o">=</span> <span class="n">p</span> <span class="o">+</span> <span class="n">i</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="n">OPT_MSGTYPE</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">msgtype</span> <span class="o">=</span> <span class="n">val</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">has_msgtype</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="n">OPT_VENDORCLASS</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">&lt;=</span> <span class="mi">255</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">memcpy</span><span class="p">(</span><span class="n">o</span><span class="o">-&gt;</span><span class="n">vendor</span><span class="p">,</span> <span class="n">val</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">vendor</span><span class="p">[</span><span class="n">len</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">vendor_len</span> <span class="o">=</span> <span class="n">len</span><span class="p">;</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">has_vendor</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="n">OPT_REQ_IP</span> <span class="o">&amp;&amp;</span> <span class="n">len</span> <span class="o">==</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">o</span><span class="o">-&gt;</span><span class="n">req_ip</span><span class="p">,</span> <span class="n">val</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
            <span class="n">o</span><span class="o">-&gt;</span><span class="n">has_req_ip</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">i</span> <span class="o">+=</span> <span class="n">len</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="nf">opt_put</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">w</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">code</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">val</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">*</span><span class="n">w</span><span class="o">++</span> <span class="o">=</span> <span class="n">code</span><span class="p">;</span>
    <span class="o">*</span><span class="n">w</span><span class="o">++</span> <span class="o">=</span> <span class="n">len</span><span class="p">;</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">val</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">w</span> <span class="o">+</span> <span class="n">len</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">size_t</span> <span class="nf">build_reply</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">out</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">size_t</span> <span class="n">outcap</span><span class="p">,</span>
                          <span class="k">const</span> <span class="n">bootp_t</span> <span class="o">*</span><span class="n">req</span><span class="p">,</span>
                          <span class="k">const</span> <span class="n">dhcp_opts_t</span> <span class="o">*</span><span class="n">reqo</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">dhcp_type</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">server_ip</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">yiaddr</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">subnet_mask</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">router_ip</span><span class="p">,</span>
                          <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">lease_seconds</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">outcap</span> <span class="o">&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">bootp_t</span><span class="p">)</span> <span class="o">+</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">64</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">bootp_t</span> <span class="n">rep</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>

    <span class="n">rep</span><span class="p">.</span><span class="n">op</span> <span class="o">=</span> <span class="n">BOOTREPLY</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">htype</span> <span class="o">=</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">htype</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">hlen</span> <span class="o">=</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">hlen</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">hops</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">xid</span> <span class="o">=</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">xid</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">secs</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">flags</span> <span class="o">=</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">flags</span><span class="p">;</span> <span class="c1">// Echo client flags</span>

    <span class="n">rep</span><span class="p">.</span><span class="n">ciaddr</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">yiaddr</span> <span class="o">=</span> <span class="n">yiaddr</span><span class="p">;</span> <span class="c1">// Offered/acked IP</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">siaddr</span> <span class="o">=</span> <span class="n">server_ip</span><span class="p">;</span> <span class="c1">// the mothership hint</span>
    <span class="n">rep</span><span class="p">.</span><span class="n">giaddr</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">memcpy</span><span class="p">(</span><span class="n">rep</span><span class="p">.</span><span class="n">chaddr</span><span class="p">,</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">chaddr</span><span class="p">,</span> <span class="mi">16</span><span class="p">);</span>

    <span class="n">memcpy</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rep</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rep</span><span class="p">));</span>
    <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">w</span> <span class="o">=</span> <span class="n">out</span> <span class="o">+</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rep</span><span class="p">);</span>

    <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">cookie</span> <span class="o">=</span> <span class="n">htonl</span><span class="p">(</span><span class="n">DHCP_COOKIE</span><span class="p">);</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">cookie</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
    <span class="n">w</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">;</span>

    <span class="c1">// Options</span>
    <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_MSGTYPE</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dhcp_type</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_SERVERID</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">server_ip</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>

    <span class="k">const</span> <span class="kt">uint32_t</span> <span class="n">lt</span> <span class="o">=</span> <span class="n">htonl</span><span class="p">(</span><span class="n">lease_seconds</span><span class="p">);</span>
    <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_LEASETIME</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">lt</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>

    <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_SUBNETMASK</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">subnet_mask</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
    <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_ROUTER</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">router_ip</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>

    <span class="c1">// Optional: echo vendor class back, may help with “vendor-aware feel”</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">reqo</span><span class="o">-&gt;</span><span class="n">has_vendor</span> <span class="o">&amp;&amp;</span> <span class="n">reqo</span><span class="o">-&gt;</span><span class="n">vendor_len</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">w</span> <span class="o">=</span> <span class="n">opt_put</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">OPT_VENDORCLASS</span><span class="p">,</span> <span class="n">reqo</span><span class="o">-&gt;</span><span class="n">vendor</span><span class="p">,</span> <span class="n">reqo</span><span class="o">-&gt;</span><span class="n">vendor_len</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="o">*</span><span class="n">w</span><span class="o">++</span> <span class="o">=</span> <span class="n">OPT_END</span><span class="p">;</span>

    <span class="k">return</span> <span class="p">(</span><span class="kt">size_t</span><span class="p">)</span> <span class="p">(</span><span class="n">w</span> <span class="o">-</span> <span class="n">out</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">!=</span> <span class="mi">6</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span>
            <span class="s">"Usage: %s &lt;ifname&gt; &lt;server_ip&gt; &lt;offer_ip&gt; &lt;subnet_mask&gt; &lt;router_ip&gt;</span><span class="se">\n</span><span class="s">"</span>
            <span class="s">"Example: %s enp2s0 192.168.100.1 192.168.100.50 255.255.255.0 192.168.100.1</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
            <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
        <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">ifname</span> <span class="o">=</span> <span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
    <span class="kt">uint32_t</span> <span class="n">server_ip</span><span class="p">,</span> <span class="n">offer_ip</span><span class="p">,</span> <span class="n">mask_ip</span><span class="p">,</span> <span class="n">router_ip</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">parse_ipv4</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">server_ip</span><span class="p">)</span> <span class="o">||</span>
        <span class="o">!</span><span class="n">parse_ipv4</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">offer_ip</span><span class="p">)</span>  <span class="o">||</span>
        <span class="o">!</span><span class="n">parse_ipv4</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">mask_ip</span><span class="p">)</span>   <span class="o">||</span>
        <span class="o">!</span><span class="n">parse_ipv4</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">5</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">router_ip</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Invalid IPv4 argument</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">2</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">const</span> <span class="kt">int</span> <span class="n">s</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">SOCK_DGRAM</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">s</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">die</span><span class="p">(</span><span class="s">"socket"</span><span class="p">);</span>

    <span class="k">const</span> <span class="kt">int</span> <span class="n">yes</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">SOL_SOCKET</span><span class="p">,</span> <span class="n">SO_REUSEADDR</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">yes</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">yes</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">die</span><span class="p">(</span><span class="s">"setsockopt REUSEADDR"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">SOL_SOCKET</span><span class="p">,</span> <span class="n">SO_BROADCAST</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">yes</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">yes</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="n">die</span><span class="p">(</span><span class="s">"setsockopt BROADCAST"</span><span class="p">);</span>

    <span class="c1">// Bind to port 67 on all addresses</span>
    <span class="k">struct</span> <span class="n">sockaddr_in</span> <span class="n">bindaddr</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
    <span class="n">bindaddr</span><span class="p">.</span><span class="n">sin_family</span> <span class="o">=</span> <span class="n">AF_INET</span><span class="p">;</span>
    <span class="n">bindaddr</span><span class="p">.</span><span class="n">sin_port</span> <span class="o">=</span> <span class="n">htons</span><span class="p">(</span><span class="mi">67</span><span class="p">);</span>
    <span class="n">bindaddr</span><span class="p">.</span><span class="n">sin_addr</span><span class="p">.</span><span class="n">s_addr</span> <span class="o">=</span> <span class="n">htonl</span><span class="p">(</span><span class="n">INADDR_ANY</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">bind</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span><span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">bindaddr</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">bindaddr</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"bind(:67) failed: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">strerror</span><span class="p">(</span><span class="n">errno</span><span class="p">));</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Tip: run as root or grant cap_net_bind_service.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Pin to interface so we don't respond on the wrong NIC.</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">SOL_SOCKET</span><span class="p">,</span> <span class="n">SO_BINDTODEVICE</span><span class="p">,</span> <span class="n">ifname</span><span class="p">,</span> <span class="n">strlen</span><span class="p">(</span><span class="n">ifname</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"SO_BINDTODEVICE(%s) failed: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ifname</span><span class="p">,</span> <span class="n">strerror</span><span class="p">(</span><span class="n">errno</span><span class="p">));</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Listening on %s UDP/67. server=%s offer=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ifname</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">argv</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span>

    <span class="kt">uint8_t</span> <span class="n">inbuf</span><span class="p">[</span><span class="mi">2048</span><span class="p">];</span>
    <span class="kt">uint8_t</span> <span class="n">outbuf</span><span class="p">[</span><span class="mi">2048</span><span class="p">];</span>

    <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">sockaddr_in</span> <span class="n">src</span><span class="p">;</span>
        <span class="n">socklen_t</span> <span class="n">srclen</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">src</span><span class="p">);</span>
        <span class="k">const</span> <span class="kt">ssize_t</span> <span class="n">n</span> <span class="o">=</span> <span class="n">recvfrom</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">inbuf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">inbuf</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span><span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">src</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">srclen</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">errno</span> <span class="o">==</span> <span class="n">EINTR</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
            <span class="n">die</span><span class="p">(</span><span class="s">"recvfrom"</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"got %zd bytes from %s:%d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
        <span class="n">n</span><span class="p">,</span> <span class="n">inet_ntoa</span><span class="p">(</span><span class="n">src</span><span class="p">.</span><span class="n">sin_addr</span><span class="p">),</span> <span class="n">ntohs</span><span class="p">(</span><span class="n">src</span><span class="p">.</span><span class="n">sin_port</span><span class="p">));</span>
        <span class="n">fflush</span><span class="p">(</span><span class="n">stderr</span><span class="p">);</span>
        <span class="k">enum</span> <span class="p">{</span> <span class="n">BOOTP_FIXED_LEN</span> <span class="o">=</span> <span class="mi">236</span> <span class="p">};</span>

        <span class="k">if</span> <span class="p">((</span><span class="kt">size_t</span><span class="p">)</span><span class="n">n</span> <span class="o">&lt;</span> <span class="n">BOOTP_FIXED_LEN</span> <span class="o">+</span> <span class="mi">4</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">size_t</span> <span class="n">opt_len</span> <span class="o">=</span> <span class="p">(</span><span class="kt">size_t</span><span class="p">)</span><span class="n">n</span> <span class="o">-</span> <span class="n">BOOTP_FIXED_LEN</span><span class="p">;</span>

        <span class="k">const</span> <span class="n">bootp_t</span> <span class="o">*</span><span class="n">req</span> <span class="o">=</span> <span class="p">(</span><span class="k">const</span> <span class="n">bootp_t</span> <span class="o">*</span><span class="p">)</span><span class="n">inbuf</span><span class="p">;</span> <span class="c1">// prolly OK for chaddr/xid/etc.</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">op</span> <span class="o">!=</span> <span class="n">BOOTREQUEST</span><span class="p">)</span> <span class="p">{</span> <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span><span class="s">"drop: not BOOTREQUEST</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span> <span class="k">continue</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">htype</span> <span class="o">!=</span> <span class="n">HTYPE_ETHERNET</span><span class="p">)</span> <span class="p">{</span> <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span><span class="s">"drop: htype=%u</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">htype</span><span class="p">);</span> <span class="k">continue</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">hlen</span> <span class="o">!=</span> <span class="n">HLEN_ETHERNET</span><span class="p">)</span> <span class="p">{</span> <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span><span class="s">"drop: hlen=%u</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">req</span><span class="o">-&gt;</span><span class="n">hlen</span><span class="p">);</span> <span class="k">continue</span><span class="p">;</span> <span class="p">}</span>

        <span class="c1">// Options start at offset 236</span>
        <span class="n">dhcp_opts_t</span> <span class="n">o</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">parse_dhcp_options</span><span class="p">(</span><span class="n">inbuf</span> <span class="o">+</span> <span class="n">BOOTP_FIXED_LEN</span><span class="p">,</span> <span class="n">opt_len</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">o</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">o</span><span class="p">.</span><span class="n">has_msgtype</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span><span class="s">"drop: no option 53</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="kt">char</span> <span class="n">macs</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
        <span class="n">print_mac</span><span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">chaddr</span><span class="p">,</span> <span class="n">macs</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">macs</span><span class="p">));</span>

        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"opts[0..15]: "</span><span class="p">);</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">16</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="kt">size_t</span><span class="p">)</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">opt_len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"%02x "</span><span class="p">,</span> <span class="n">inbuf</span><span class="p">[</span><span class="n">BOOTP_FIXED_LEN</span> <span class="o">+</span> <span class="n">i</span><span class="p">]);</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

        <span class="kt">uint8_t</span> <span class="n">reply_type</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">msgtype</span> <span class="o">==</span> <span class="n">DHCPDISCOVER</span><span class="p">)</span> <span class="n">reply_type</span> <span class="o">=</span> <span class="n">DHCPOFFER</span><span class="p">;</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">msgtype</span> <span class="o">==</span> <span class="n">DHCPREQUEST</span><span class="p">)</span> <span class="n">reply_type</span> <span class="o">=</span> <span class="n">DHCPACK</span><span class="p">;</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"drop: Unknown msgtype 0x%02x</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">o</span><span class="p">.</span><span class="n">msgtype</span><span class="p">);</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Honor Option 50 Requested IP if present.</span>
        <span class="kt">uint32_t</span> <span class="n">yiaddr</span> <span class="o">=</span> <span class="n">offer_ip</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">has_req_ip</span><span class="p">)</span> <span class="n">yiaddr</span> <span class="o">=</span> <span class="n">o</span><span class="p">.</span><span class="n">req_ip</span><span class="p">;</span>

        <span class="kt">size_t</span> <span class="n">outlen</span> <span class="o">=</span> <span class="n">build_reply</span><span class="p">(</span><span class="n">outbuf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">outbuf</span><span class="p">),</span> <span class="n">req</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">o</span><span class="p">,</span> <span class="n">reply_type</span><span class="p">,</span>
                                    <span class="n">server_ip</span><span class="p">,</span> <span class="n">yiaddr</span><span class="p">,</span> <span class="n">mask_ip</span><span class="p">,</span> <span class="n">router_ip</span><span class="p">,</span> <span class="mi">3600</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">outlen</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>

        <span class="c1">// Send to broadcast:68, as we noticed the client doesn't have an IP</span>
        <span class="k">struct</span> <span class="n">sockaddr_in</span> <span class="n">dst</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
        <span class="n">dst</span><span class="p">.</span><span class="n">sin_family</span> <span class="o">=</span> <span class="n">AF_INET</span><span class="p">;</span>
        <span class="n">dst</span><span class="p">.</span><span class="n">sin_port</span> <span class="o">=</span> <span class="n">htons</span><span class="p">(</span><span class="mi">68</span><span class="p">);</span>
        <span class="n">dst</span><span class="p">.</span><span class="n">sin_addr</span><span class="p">.</span><span class="n">s_addr</span> <span class="o">=</span> <span class="n">htonl</span><span class="p">(</span><span class="n">INADDR_BROADCAST</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">sendto</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="n">outbuf</span><span class="p">,</span> <span class="n">outlen</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span><span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">dst</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">dst</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"sendto failed: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">strerror</span><span class="p">(</span><span class="n">errno</span><span class="p">));</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Sent %lu bytes</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">outlen</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">has_vendor</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"%s xid=%08x -&gt; %s (vendor=%s)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
                    <span class="n">macs</span><span class="p">,</span> <span class="n">ntohl</span><span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">xid</span><span class="p">),</span>
                    <span class="p">(</span><span class="n">reply_type</span> <span class="o">==</span> <span class="n">DHCPOFFER</span><span class="p">)</span> <span class="o">?</span> <span class="s">"OFFER"</span> <span class="o">:</span> <span class="s">"ACK"</span><span class="p">,</span>
                    <span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">o</span><span class="p">.</span><span class="n">vendor</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"%s xid=%08x -&gt; %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
                    <span class="n">macs</span><span class="p">,</span> <span class="n">ntohl</span><span class="p">(</span><span class="n">req</span><span class="o">-&gt;</span><span class="n">xid</span><span class="p">),</span>
                    <span class="p">(</span><span class="n">reply_type</span> <span class="o">==</span> <span class="n">DHCPOFFER</span><span class="p">)</span> <span class="o">?</span> <span class="s">"OFFER"</span> <span class="o">:</span> <span class="s">"ACK"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>

</details>

<p>This replies with what the Ray expects, offering it an IP leased from the Linux box. I
only forgot to mention how things are wired here at my lab:</p>

<p>Sun Ray ↔ USB Ethernet ↔ Linux box (DHCP responder) ↔ rest of LAN</p>

<p>So the magic is happening on interface <code class="language-plaintext highlighter-rouge">enx00e04c3f6690</code>, I’ll give myself an IP
<code class="language-plaintext highlighter-rouge">192.168.100.1</code>, and give <code class="language-plaintext highlighter-rouge">192.168.100.50</code> to the Ray. First, let’s get an IP on
that interface:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo ip addr add 192.168.100.1/24 dev enx00e04c3f6690
sudo ip link set enx00e04c3f6690 up
</code></pre></div></div>

<p>And finally compile and run (as <code class="language-plaintext highlighter-rouge">sudo</code>). The result is positive! The Ray updated
the UI indicating it has communicated with the server! Then, a wild UDP packet
appears:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0000   ff ff ff ff ff ff 00 14 4f 85 f2 0b 08 00 45 00   ........O.....E.
0010   00 27 00 06 40 00 40 11 15 e6 c0 a8 64 32 ff ff   .'..@.@.....d2..
0020   ff ff 1b 61 1b 61 00 13 00 00 73 65 72 76 65 72   ...a.a....server
0030   51 3d 31 0a 00 00 00 00 00 00 00 00               Q=1.........
</code></pre></div></div>

<p>Ignoring the framing information and other headers we get 11 payload bytes plus
some padding. The payload is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0000 73 65 72 76 65 72 51 3d 31 0a 00                   serverQ=1..
</code></pre></div></div>

<p>That’s… Literally <code class="language-plaintext highlighter-rouge">serverQ=1\n\0</code>. But there’s more! Here’s what the packet
gives:</p>

<ol>
  <li>Both source and destination ports are <code class="language-plaintext highlighter-rouge">7009</code></li>
  <li>The source IP is the IP we gave to the Ray! (I omitted a few <code class="language-plaintext highlighter-rouge">DHCPACK</code> and <code class="language-plaintext highlighter-rouge">DHCPINFORM</code> packets)</li>
  <li>The destination IP is STILL the broadcast address</li>
</ol>

<p>This might be Sun Ray’s appliance protocol territory (UDP/7009 shows up in Sun
Ray land), but I’m treating it as an unknown for now. The protocol is
proprietary and not documented, but let’s wrap up for now and just be happy for
the little, still important, progress.</p>

<p>Next: what happens if we answer <code class="language-plaintext highlighter-rouge">serverQ=1</code>?</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This Christmas I just got a brand new, still sealed, Sun Ray 2. Basically the best gift I ever got. Period.]]></summary></entry><entry><title type="html">Leveraging LLMs for what LLMs are worth</title><link href="https://vito.io/articles/2025-12-08-leveraging-llms-for-what-llms-are-worth.html" rel="alternate" type="text/html" title="Leveraging LLMs for what LLMs are worth" /><published>2025-12-08T00:00:00-03:00</published><updated>2025-12-08T00:00:00-03:00</updated><id>https://vito.io/articles/leveraging-llms-for-what-llms-are-worth</id><content type="html" xml:base="https://vito.io/articles/2025-12-08-leveraging-llms-for-what-llms-are-worth.html"><![CDATA[<p>Those who work with me know for sure of two important points:</p>

<ol>
  <li>I despise LLMs. I don’t think they should replace or even try to replace
people.</li>
  <li>I am a big fan of well-written RFCs and specifications.</li>
</ol>

<p>The first point is not a surprise; being a part of this industry before the
advent of LLMs gave me enough resources to be able to compare how things used
to work, and how they work now. The field is clearly divided, though, but I see
it as as binary as ever: either engineers love it, or despise it. I feel that
people are still in awe of the possibility to have something else capable of,
for instance, to write unit tests for a feature, but they fail to understand
that the one who wrote the implementation must also be the one who writes the
tests, as albeit manual, the process is still important, since it forces one to
use what they implemented. In this step, several extremely relevant points can
still be observed:</p>

<ul>
  <li>The ergonomics of the API. Is it cohesive? How hard is it for someone to use it?</li>
  <li>Its documentation. Is it documented at all? Will someone that has absolutely no
context of the whole be able to still understand it and use without friction?</li>
  <li>Is the domain properly segregated? Is the API requiring more than it should?</li>
  <li>Are names descriptive enough? Do they make sense when used with other components?</li>
</ul>

<p>Those are just a glimpse of what writing both tests and documentation can
provide to the one who is implementing it, but the list goes on. Now, delegating
the job to a tool whose reasoning is not even visible blinds implementors from
those nuances.</p>

<p>I also see how this is deeply affecting those who are entering the field. For
instance, when mentoring anyone, the most important thing I keep repeating on
and on is: Read and understand error messages, and do not ignore warnings. I see
often people just acknowledging message boxes on UIs, hitting OK without
even reading it, and then asking themselves “Why isn’t this working?”; the same
applies to developers. One must be able to understand what an error message
means, and how to read a stack trace. Now, imagine someone in university, or in
an internship blindly writing code, and asking an LLM to interpret any errors
and fix them. What will this person be able to accomplish without it?</p>

<p>But this is not only affecting newcomers. I often see interesting cases coming
from people who are part of the field for a while:</p>

<ul>
  <li>“Your code is wrong, see what <insert your="" favourite="" LLM="" here=""> said: ...";
meaning "I haven't read your code, I have no idea what it does, and I'm acting
as a proxy to an LLM".</insert></li>
  <li>“I think we should use <insert complex="" technology="" here="">, see my chat
transcription": meaning "I have no domain nor property, but you should use what
the machine told me"</insert></li>
  <li>I opened a Pull Request, but I have no idea what it does, as seen in the
<a href="https://gitlab.freedesktop.org/mesa/mesa/-/work_items/13736">infamous Mesa Issue</a>.</li>
  <li>“There’s something wrong, see what I got regarding this issue using <LLM>".</LLM></li>
</ul>

<p>Sometimes I feel like people can’t even read and maintain their own code, let
alone code generated by an LLM, copy-and-pasted into the codebase; I don’t need
to say that’s dangerous not only on a security point-of-view, but on a business
continuation one.</p>

<p>This ties directly into my second point about well-written specifications, which
exist precisely to prevent this kind of ambiguity and dependency on individual
authors.</p>

<p>The second point is also not a surprise. I’m a big fan of LaTeX, and a bigger
fan of IETF and everything they achieved. I work with the premise that every RFC
and specification must be written in a fashion that allows anyone reading it to
fully understand what it is about, and even more important: to be able to
implement it no matter the language or technology they want to use. This way,
even if I leave the company I’m working on, the documentation is there, all the
design decisions are still there, and are both navigable and clear to the point
where they don’t need me, the original author, to be able to update or
reimplement it: everything is within a single documentation unit.</p>

<p>Now, where can LLMs help us here? Not designing documentation, not writing code,
but reviewing the documents. It turns out that LLMs are great reviewers, since
they can keep a lot in their context, and are also able to comprehend documents
in a matter of seconds. However, there’s an important pitfall in leveraging
hosted LLMs (like ChatGPT or Claude): one must be absolutely sure of their data
policies, and even more important, that the company will not use anything you
provided to train future versions. So that is how I use LLMs: for reviewing what
I write.</p>

<p>Given their ability to quickly cross-examine a whole document in seconds, I
mostly use it as part of my writing process. First, I draft the document, put
everything I need, and make the first review. When I’m happy with the result,
it’s time to get a critic. I allow the LLM to read it, and ask for feedback on
sections that need development, for points that are not clear or ambiguous, for
contradictions and typos. And they are excellent at this. They can pinpoint
which section makes a contradictory statement against another, potentially
incomplete definitions, examples or small details that are present in an ABNF
section, but explained incorrectly in the body of text, along other small nits
and inconsistencies that can truly elevate the level of the document. But do
notice how it didn’t replace the creation process, it is not replacing the
brain, only augmenting it to the point where one would forget that a small
sentence ten sections earlier didn’t match what’s written in the end of the
document.</p>

<p>That’s what people should be using LLMs for. For augmenting our abilities. Not
replacing them. Renata Martins wrote a few months ago a post with a provocative
title: <a href="https://renata.codes/articles/2025-08-30-llms-are-the-lobotomy-of-the-21st-century">LLMs are the Lobotomy of the 21st Century</a>,
and time and time again I fear that it may be true.</p>

<p>This post diverged from what I’m used to writing, but I had this on my head for
so long, that I thought it was worth putting out.</p>

<p>For now, let’s continue using our brains as we should, and avoid atrophying
them; if we use LLMs thoughtfully, they can strengthen (rather than weaken)
our engineering practices.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Those who work with me know for sure of two important points:]]></summary></entry><entry><title type="html">A Better Conventional Commit?</title><link href="https://vito.io/articles/2025-07-07-a-better-conventional-commit.html" rel="alternate" type="text/html" title="A Better Conventional Commit?" /><published>2025-07-07T00:00:00-03:00</published><updated>2025-07-07T00:00:00-03:00</updated><id>https://vito.io/articles/a-better-conventional-commit</id><content type="html" xml:base="https://vito.io/articles/2025-07-07-a-better-conventional-commit.html"><![CDATA[<p>Today I found a post from <a href="https://srazkvt.codeberg.page/posts/2025-07-06-conventional-commits-makes-me-sad.html">Sarah Mathey</a> that makes very good points regarding conventional commits. I’ll quote a few here:</p>

<blockquote>
  <p>According to the git-commit manpage, it’s “a good idea to begin the commit message with a single short (no more than 50 characters)”. This commit message lands at 71 characters.
Personally, I would have put something like “Fix array parsing when input has multiple spaces”. Smaller, and also complies with recommendations from the git documentation.</p>
</blockquote>

<blockquote>
  <p>The type of a commit here is only really useful for automated tools, usually the type can be inferred from the summary of the commit. As such, I think the type would be better off as a field in footer of commit, under a key such as Commit-Type or Type.</p>
</blockquote>

<blockquote>
  <p>But why make an exception for BREAKING CHANGE ? Why not have it as Breaking-Change ? It’s not like it adds or remove any character, all that would change is it would stay consistent with other keys.</p>
</blockquote>

<p>That makes so much sense. And folks, we’re just a <code class="language-plaintext highlighter-rouge">--trailer</code> away from making this work as it should.</p>

<p>Now for the facts: I do use conventional commits, as it is — as Sarah said — useful for automated tools, and every time I make a commit message larger than it should be, I do feel bad. However, adding <code class="language-plaintext highlighter-rouge">--trailer</code> for each commit and for each property would be cumbersome. So why not make a wrapper for <code class="language-plaintext highlighter-rouge">git commit</code> to handle that?</p>

<h2 id="the-idea">The Idea</h2>

<p>What if we could do something like <code class="language-plaintext highlighter-rouge">git commit --chore --breaking -m "Message here"</code> and get those fancy trailers? Of course, we have some commits like <code class="language-plaintext highlighter-rouge">chore(some-component): Some description</code>, so let’s also allow <code class="language-plaintext highlighter-rouge">--chore</code> to accept an optional parameter. I’m low on imagination today, so let’s call this “Pretty Commit”, and alias it as <code class="language-plaintext highlighter-rouge">pc</code>. Here are some possibilities:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git pc <span class="nt">--chore</span> component <span class="nt">-m</span> <span class="s2">"Commit message"</span>
<span class="c"># or</span>
git pc <span class="nt">--spec</span> <span class="nt">-m</span> <span class="s2">"Commit message"</span>
<span class="c"># or even</span>
git pc <span class="nt">--feat</span> <span class="s2">"Tokenizer"</span> <span class="nt">--breaking</span> <span class="nt">-m</span> <span class="s2">"Allow tokenizer to do a backflip"</span>
</code></pre></div></div>

<p>And the result from the last commit:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>commit 4decde3d186a834a092676d3143fae6f7a54945a (HEAD -&gt; master)
Author: Vito Sartori &lt;hey@vito.io&gt;
Date:   Mon Jul 7 10:50:22 2025 -0300

    Allow tokenizer to do a backflip

    Type: feat
    Scope: Tokenizer
    Breaking: true
</code></pre></div></div>

<p>All parseable! Let’s, for instance, get the meta from this commit:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> λ | git log -1 --pretty=format:"%B" | git interpret-trailers --parse
Type: feat
Scope: Tokenizer
Breaking: true
</code></pre></div></div>

<p>That’s so much easier to parse than extracting stuff from the commit message alone. Git plumbing has everything everyone ever needed.</p>

<p>I’ll try using this approach and also use it as a reason to bump <a href="https://github.com/heyvito/semver-releaser">heyvito/semver-releaser</a>.</p>

<h2 id="can-i-use-it">Can I use it?</h2>

<p>Of course! Git is composable, meaning you just need to have <code class="language-plaintext highlighter-rouge">git-pc</code> in your <code class="language-plaintext highlighter-rouge">PATH</code>. Then <code class="language-plaintext highlighter-rouge">git pc</code> is immediately available.
The source is available at <a href="https://github.com/heyvito/git-pc">github.com/heyvito/git-pc</a>, and is a (very) simple bash script — as far as ‘simple’ and ‘bash’ can go together.</p>

<p>Contributions are welcome!</p>

<h2 id="what-else">What else?</h2>

<p>If you didn’t, <a href="https://srazkvt.codeberg.page/posts/2025-07-06-conventional-commits-makes-me-sad.html">read Sarah’s post</a>. It’s full of insights and was what made me do this. Also, Tyler Cipriani posted <a href="https://tylercipriani.com/blog/2022/11/19/git-notes-gits-coolest-most-unloved-feature/">about Git Notes</a>, which is another feature that goes unnoticed.</p>

<p>And that’s it for today. Let’s see if we can push this forward!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Today I found a post from Sarah Mathey that makes very good points regarding conventional commits. I’ll quote a few here:]]></summary></entry><entry><title type="html">Goodbye, Arc</title><link href="https://vito.io/articles/2025-05-21-goodbye-arc.html" rel="alternate" type="text/html" title="Goodbye, Arc" /><published>2025-05-21T00:00:00-03:00</published><updated>2025-05-21T00:00:00-03:00</updated><id>https://vito.io/articles/goodbye-arc</id><content type="html" xml:base="https://vito.io/articles/2025-05-21-goodbye-arc.html"><![CDATA[<p>After being a long-time Firefox user and fan, in 2022 I decided to switch to
Arc, given their promise to make the web better — although using the Chromium
engine, which isn’t better <em>per se</em>. It has been my default browser ever since,
even though I felt like I was betraying the web by leveraging Chromium.</p>

<p>Now it’s been a while since The Browser Company released a compelling update
that’s not just a bump to the internal Chromium version, and filling their
release notes with irrelevant content. Of course, they are focusing on their
<em>AI-based browser</em> Dia, but I feel betrayed, considering all the promises they
made.</p>

<p>There’s one thing that kept me on Arc, though: its sidebar. The sidebar was a
great concept, and I could simply drag-and-drop tabs to read later, and it would
just sync across my computers. That was great — until it became a mess. My
sidebar is long. I mean <em>really long</em>. And searching/exploring it became hard.
Adding all that up, I felt it was time to switch back to Firefox — and switch
back I did.</p>

<p>But, what about my sidebar? Well, Arc does not offer a way to export the sidebar,
but luckily, it keeps it in a JSON file in its <code class="language-plaintext highlighter-rouge">Application Support</code> directory.
So a quick Ruby script later, all my links were exported. The point then was:
<em>where to place those items?</em>.</p>

<h2 id="fixing-my-bookmarks-issue">Fixing my Bookmarks Issue</h2>

<p>The first place I looked was <code class="language-plaintext highlighter-rouge">raindrop.io</code>, which I used for a while after it
launched. Raindrop is great — really — but it has <em>one small issue</em> that I can’t
stand: It does not permanently store YouTube videos — which makes sense, I think.</p>

<p>So I started working on my own solution to that: Rio (Retain Information Onshore).
Rio takes a URL or file from my system, processes it, and permanently stores it,
be it a web page, PDF, docx, or video. With a little help from Llama/Mistral, PDFs
are correctly summarized and have their title extracted, so everything stays
organized.</p>

<p>I have plans to also add a Solr integration for full-text search, but that will have
to wait longer.</p>

<p>This is somewhat a big project, and may be interesting to other people who like
to self-host stuff, so eventually it may become open-source. Who knows :)</p>

<p>So, so long Arc. It was good while you lasted. Let’s rock Firefox Nightly and
keep the web balanced.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[After being a long-time Firefox user and fan, in 2022 I decided to switch to Arc, given their promise to make the web better — although using the Chromium engine, which isn’t better per se. It has been my default browser ever since, even though I felt like I was betraying the web by leveraging Chromium.]]></summary></entry><entry><title type="html">NPM Registry HTTP Semantics</title><link href="https://vito.io/articles/2025-01-30-npm-registry-http-semantics.html" rel="alternate" type="text/html" title="NPM Registry HTTP Semantics" /><published>2025-01-30T00:00:00-03:00</published><updated>2025-01-30T00:00:00-03:00</updated><id>https://vito.io/articles/npm-registry-http-semantics</id><content type="html" xml:base="https://vito.io/articles/2025-01-30-npm-registry-http-semantics.html"><![CDATA[<p>Here at <span class="redacted">nice try</span>, I’m working on an NPM proxy
and internal registry. All is fine and dandy until you notice that NPM Registry,
although returning <code class="language-plaintext highlighter-rouge">ETag</code> headers, simply ignore them. For instance, let’s try
get the latest version of say, <code class="language-plaintext highlighter-rouge">react</code>, and then use <code class="language-plaintext highlighter-rouge">If-None-Match</code> to get an
indicator our request is still fresh.</p>

<h3 id="first-request-no-if-none-match">First Request: No <code class="language-plaintext highlighter-rouge">If-None-Match</code></h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /react HTTP/1.1
Host: registry.npmjs.org
Connection: close
User-Agent: [REDACTED]/0.1.0
</code></pre></div></div>

<p>The response is expected:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Date: Thu, 30 Jan 2025 12:35:30 GMT
Content-Type: application/json
Content-Length: 5387821
Connection: close
CF-Ray: 90a18092cb3802e5-GRU
CF-Cache-Status: HIT
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Age: 75
Cache-Control: public, max-age=300
ETag: "5611f329debbb41fda058497d0d4c7d8"
Last-Modified: Wed, 29 Jan 2025 16:20:20 GMT
Vary: accept-encoding, accept
Server: cloudflare
</code></pre></div></div>

<p>Fancy stuff. Very nice. Even an <code class="language-plaintext highlighter-rouge">ETag</code>! So the expectation is that <code class="language-plaintext highlighter-rouge">If-None-Match</code>
yields an HTTP 304, right? Let’s try it again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /react HTTP/1.1
If-None-Match: "5611f329debbb41fda058497d0d4c7d8"
Host: registry.npmjs.org
Connection: close
User-Agent: [REDACTED]/0.1.0
</code></pre></div></div>

<p>And…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Date: Thu, 30 Jan 2025 12:37:37 GMT
Content-Type: application/json
Content-Length: 5387821
Connection: close
CF-Ray: 90a183af0d59ae90-GRU
CF-Cache-Status: HIT
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Age: 202
Cache-Control: public, max-age=300
ETag: "5611f329debbb41fda058497d0d4c7d8"
Last-Modified: Wed, 29 Jan 2025 16:20:20 GMT
Vary: accept-encoding, accept
Server: cloudflare
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">OK</code>? My <code class="language-plaintext highlighter-rouge">If-None-Match</code> is extactly the same <code class="language-plaintext highlighter-rouge">ETag</code> returned on the response.</p>

<h3 id="second-request-should-head-help-us">Second Request: Should <code class="language-plaintext highlighter-rouge">HEAD</code> help us?</h3>

<p>Okay, but WHAT IF we issued a <code class="language-plaintext highlighter-rouge">HEAD</code> in order to compare <code class="language-plaintext highlighter-rouge">ETag</code>s? That would be
enough to assert we have the same payload, right?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HEAD /react HTTP/1.1
Host: registry.npmjs.org
Connection: close
User-Agent: [REDACTED]/0.1.0
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK
Date: Thu, 30 Jan 2025 12:39:41 GMT
Content-Type: application/json
Connection: close
CF-Ray: 90a186b3687af23f-GRU
CF-Cache-Status: HIT
Accept-Ranges: bytes
Access-Control-Allow-Origin: *
Age: 24
Cache-Control: public, max-age=300
ETag: W/"5611f329debbb41fda058497d0d4c7d8"
Last-Modified: Wed, 29 Jan 2025 16:20:20 GMT
Vary: accept-encoding, accept
Server: cloudflare
</code></pre></div></div>

<p>Wha-What? A Weak ETag? How? Why?</p>

<h3 id="how-npm-cli-handles-this">How NPM CLI handles this</h3>

<p>After some digging, it seems NPM’s CLI is quite… simple. It internally caches
the contents for 5 minutes. After that, it just downloads it again.</p>

<h3 id="workarounds">Workarounds</h3>

<p>We <em>could</em> (which does not mean we <em>should</em>) read the initial headers and drop
the connection, but that does not mean we won’t receive the payload on a <code class="language-plaintext highlighter-rouge">GET</code>
request, we will only be breaking HTTP semantics. After all, the remote will be
shoving the payload to our connection nonetheless.</p>

<p>Another option would be perform the same <code class="language-plaintext highlighter-rouge">GET</code>, but using <code class="language-plaintext highlighter-rouge">Range</code> to not
get a body, as it seems the server supports it. Let’s try:</p>

<h3 id="leveraging-range-header">Leveraging <code class="language-plaintext highlighter-rouge">Range</code> Header</h3>

<p>The server seems to support byte ranges, as announced by the header
<code class="language-plaintext highlighter-rouge">Accept-Ranges: bytes</code>. So let’s ask for a single byte. We will waste a single
byte, but at least it’s not 30-50MB depending on the manifest. I’ll keep the
<code class="language-plaintext highlighter-rouge">If-None-Match</code> just for extra measure.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /react HTTP/1.1
If-None-Match: "5611f329debbb41fda058497d0d4c7d8"
Range: bytes=0-0
Host: registry.npmjs.org
Connection: close
User-Agent: [REDACTED]/0.1.0
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 206 Partial Content
Date: Thu, 30 Jan 2025 12:54:36 GMT
Content-Type: application/json
Content-Length: 1
Connection: close
Content-Range: bytes 0-0/5387821
CF-Ray: 90a19c9078a51b24-GRU
CF-Cache-Status: HIT
Access-Control-Allow-Origin: *
Age: 11
Cache-Control: public, max-age=300
ETag: "5611f329debbb41fda058497d0d4c7d8"
Last-Modified: Wed, 29 Jan 2025 16:20:20 GMT
Vary: accept-encoding, accept
Set-Cookie: _cfuvid=hY7zxXExF2P4mja1cX6ynhOfcoAuJzxR42SmGTiLJvY-1738241676925-0.0.1.1-604800000; path=/; domain=.npmjs.org; HttpOnly; Secure; SameSite=None
Server: cloudflare
</code></pre></div></div>

<p>Success! We got a single byte, and a <code class="language-plaintext highlighter-rouge">206</code> status response. Is that enough? I
honestly don’t think so, considering that the server returns an <code class="language-plaintext highlighter-rouge">ETag</code>. But if
that’s the only alternative, I’ll happily stick with it, although it feels a
really, <em>really</em> hacky solution.</p>

<h3 id="contacting-npm-support">Contacting NPM Support</h3>

<p>I raised a ticket with NPM support to understand what is going on, but so far, I
haven’t found a suitable alternative.</p>

<p><del>Will update this once I get a response from the fine folks at NPM.</del></p>

<p>One month later, I got the following reply from NPM Support:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>So sorry for the oversight and delay addressing your concerns.

As previously mentioned, with the behavior you're seeing with the If-None-Match
header for registry.npmjs.org, it’s not expected for it to always send the full
payload when the ETag matches. Normally, if the If-None-Match header matches the
ETag returned by the registry, the server should respond with a 304 Not Modified
status, not the full payload.

It might be worth checking if there's anything else in the request (such as
additional headers or network issues) affecting the behavior.

As we’ve reached the maximum allowable time for keeping this ticket open, we
will be closing it. However, the issue is documented and escalated it to our
engineering team for further resolution.

We will continue working on this and encourage you to check periodically for any
updates on the status. I'll reach out with any updates.

We truly appreciate understanding throughout this process.
</code></pre></div></div>

<p>So… Not fixed, and perhaps won’t be. Bummer.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Here at nice try, I’m working on an NPM proxy and internal registry. All is fine and dandy until you notice that NPM Registry, although returning ETag headers, simply ignore them. For instance, let’s try get the latest version of say, react, and then use If-None-Match to get an indicator our request is still fresh.]]></summary></entry><entry><title type="html">K8s Bare Metal</title><link href="https://vito.io/articles/2025-01-21-baremetal-k8s.html" rel="alternate" type="text/html" title="K8s Bare Metal" /><published>2025-01-21T00:00:00-03:00</published><updated>2025-01-21T00:00:00-03:00</updated><id>https://vito.io/articles/baremetal-k8s</id><content type="html" xml:base="https://vito.io/articles/2025-01-21-baremetal-k8s.html"><![CDATA[<p>I know there are so many posts on how to configure a bare-metal K8s cluster, but
after studying a lot, here’s my take on it.</p>

<p>Here we will be configuring a cluster composed by:</p>

<ul>
  <li>1 Master Node</li>
  <li>4 Worker Nodes</li>
  <li>1 NFS server so we can use storage over network</li>
</ul>

<p>I’m provisioning my cluster on ESXi that’s running behind a PFSense, so traffic
can be controlled, and DHCP does not touch my main home network. I’ll also use
an internal DNS to route everything, and will use domains <code class="language-plaintext highlighter-rouge">*.k.vito.sh</code> for
exposing services hosted in the cluster, and <code class="language-plaintext highlighter-rouge">*.kube.vito.sh</code> to access VMS.</p>

<p>All machines will be provisioned with 8GB RAM and 4 cores.
The network will look like this:</p>

<ul>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-master</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.0</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">master.kube.vito.sh</code></li>
    </ul>
  </li>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-worker1</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.1</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">w1.kube.vito.sh</code></li>
    </ul>
  </li>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-worker2</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.2</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">w2.kube.vito.sh</code></li>
    </ul>
  </li>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-worker3</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.3</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">w3.kube.vito.sh</code></li>
    </ul>
  </li>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-worker4</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.4</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">w4.kube.vito.sh</code></li>
    </ul>
  </li>
  <li>Machine: <code class="language-plaintext highlighter-rouge">k8s-nfs</code>
    <ul>
      <li>IP: <code class="language-plaintext highlighter-rouge">10.0.11.5</code></li>
      <li>DNS: <code class="language-plaintext highlighter-rouge">nfs.kube.vito.sh</code></li>
    </ul>
  </li>
</ul>

<h3 id="provisioning-nodes">Provisioning Nodes</h3>

<blockquote>
  <p><strong>Warning</strong>: You will notice I’m running several commands without sudo. I
usually just get a root shell using <code class="language-plaintext highlighter-rouge">sudo su</code> in order to execute everything
without hassle.</p>
</blockquote>

<p>All nodes except <code class="language-plaintext highlighter-rouge">k8s-nfs</code> follow the same steps:</p>

<ol>
  <li>Install Ubuntu 22.04 LTS. I use a minified server installation.</li>
  <li>Make sure everything is up-to-date:
    <ol>
      <li><code class="language-plaintext highlighter-rouge">apt update &amp;&amp; apt upgrade -y &amp;&amp; reboot</code></li>
    </ol>
  </li>
  <li>Drop <code class="language-plaintext highlighter-rouge">snap</code>
    <ol>
      <li><code class="language-plaintext highlighter-rouge">apt purge snapd</code></li>
    </ol>
  </li>
  <li>Install utilities:
    <ol>
      <li><code class="language-plaintext highlighter-rouge">apt install iputils-ping vim apt-transport-https ca-certificates curl gnupg2 gpg software-properties-common -y</code></li>
    </ol>
  </li>
</ol>

<h4 id="install-cri-o">Install Cri-O</h4>

<ol>
  <li><code class="language-plaintext highlighter-rouge">export OS=xUbuntu_22.04</code></li>
  <li><code class="language-plaintext highlighter-rouge">export CRIO_VERSION=1.26</code></li>
  <li><code class="language-plaintext highlighter-rouge">echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /"| sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list</code></li>
  <li><code class="language-plaintext highlighter-rouge">echo "deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS/ /"|sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION.list</code></li>
  <li><code class="language-plaintext highlighter-rouge">curl -L https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION/$OS/Release.key | sudo apt-key add -</code></li>
  <li><code class="language-plaintext highlighter-rouge">curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key add -</code></li>
  <li><code class="language-plaintext highlighter-rouge">apt update &amp;&amp; apt install cri-o cri-o-runc -y</code></li>
  <li><code class="language-plaintext highlighter-rouge">systemctl enable --now crio</code></li>
  <li>Check Cri-O status: <code class="language-plaintext highlighter-rouge">systemctl status crio</code></li>
</ol>

<h4 id="enable-conmon">Enable <code class="language-plaintext highlighter-rouge">conmon</code></h4>

<ol>
  <li>Edit <code class="language-plaintext highlighter-rouge">/etc/crio/crio.conf</code>, find and uncomment the line <code class="language-plaintext highlighter-rouge">conmon = ""</code></li>
  <li>Insert <code class="language-plaintext highlighter-rouge">/usr/bin/conmon</code> between the quotes</li>
  <li>Restart Cri-O: <code class="language-plaintext highlighter-rouge">systemctl restart crio</code></li>
  <li>Check its status: <code class="language-plaintext highlighter-rouge">systemctl status crio</code></li>
</ol>

<h4 id="install-cri-o-tools">Install Cri-O tools</h4>

<ol>
  <li><code class="language-plaintext highlighter-rouge">apt install -y cri-tools</code></li>
  <li>Check everything is working: <code class="language-plaintext highlighter-rouge">crictl --runtime-endpoint unix:///var/run/crio/crio.sock version</code></li>
  <li>Check if it is ready: <code class="language-plaintext highlighter-rouge">crictl info</code>. It is expected <code class="language-plaintext highlighter-rouge">RuntimeReady</code> to be <code class="language-plaintext highlighter-rouge">true</code></li>
</ol>

<h3 id="configure-networking">Configure Networking</h3>

<p>This is optional, but since I use an internal DNS, I need to set nameservers through <code class="language-plaintext highlighter-rouge">netplan</code>:</p>

<ol>
  <li>Edit <code class="language-plaintext highlighter-rouge">/etc/netplan/00-installer-config.yaml</code> and add the following line: <code class="language-plaintext highlighter-rouge">nameservers.addresses: [10.0.1.3]</code></li>
  <li>Run <code class="language-plaintext highlighter-rouge">netplan apply</code></li>
</ol>

<h3 id="prepare-apt-for-k8s-installation">Prepare APT for K8s installation</h3>

<ol>
  <li>Make sure <code class="language-plaintext highlighter-rouge">/etc/apt/keyrings</code> exist. Create it if it does not:
    <ol>
      <li><code class="language-plaintext highlighter-rouge">[ -d /etc/apt/keyrings ] || mkdir -p -m 755 /etc/apt/keyrings</code></li>
    </ol>
  </li>
  <li>Download public signing key for the K8s packages: <code class="language-plaintext highlighter-rouge">curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg</code></li>
  <li>Add the appropriate <code class="language-plaintext highlighter-rouge">apt</code> repository: <code class="language-plaintext highlighter-rouge">echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list</code></li>
  <li>Install Kubernetes components:
    <ol>
      <li><code class="language-plaintext highlighter-rouge">apt update</code></li>
      <li><code class="language-plaintext highlighter-rouge">apt install -y kubelet kubeadm kubectl</code></li>
      <li><code class="language-plaintext highlighter-rouge">apt-mark hold kubelet kubeadm kubectl</code></li>
    </ol>
  </li>
  <li>Enable and start <code class="language-plaintext highlighter-rouge">kubelet</code>: <code class="language-plaintext highlighter-rouge">sudo systemctl enable --now kubelet</code></li>
</ol>

<h3 id="update-system-configuration-for-kubernetes">Update System Configuration for Kubernetes</h3>

<ol>
  <li>Enable required kernel modules:
    <ol>
      <li><code class="language-plaintext highlighter-rouge">modprobe overlay</code></li>
      <li><code class="language-plaintext highlighter-rouge">modprobe br_netfilter</code></li>
    </ol>
  </li>
  <li>Ensure modules are loaded on restart:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo </span>overlay | <span class="nb">tee</span> <span class="nt">-a</span> /etc/modules
<span class="nb">echo </span>br_netfilter | <span class="nb">tee</span> <span class="nt">-a</span> /etc/modules
</code></pre></div></div>

<ol>
  <li>Update network configuration:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&gt;</span> /etc/sysctl.d/99-kubernetes-cri.conf <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
</span><span class="no">EOF
</span></code></pre></div></div>

<ol>
  <li>Apply system configuration: <code class="language-plaintext highlighter-rouge">sysctl --system</code></li>
  <li>Disable swap
    <ol>
      <li><code class="language-plaintext highlighter-rouge">systemctl mask swap.img.swap</code></li>
      <li><code class="language-plaintext highlighter-rouge">systemctl stop swap.img.swap</code></li>
      <li><code class="language-plaintext highlighter-rouge">swapoff -a</code></li>
    </ol>
  </li>
  <li>Update <code class="language-plaintext highlighter-rouge">kubeadm</code> configuration to use <code class="language-plaintext highlighter-rouge">systemd</code> as its <code class="language-plaintext highlighter-rouge">cgroup</code> driver:
    <ol>
      <li>Create <code class="language-plaintext highlighter-rouge">/etc/default/kubelet</code> with the following contents:</li>
    </ol>
  </li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KUBELET_EXTRA_ARGS=--cgroup-driver=systemd --runtime-request-timeout=5m
</code></pre></div></div>

<ol>
  <li>Apply changes once again
    <ol>
      <li><code class="language-plaintext highlighter-rouge">systemctl daemon-reload</code></li>
      <li><code class="language-plaintext highlighter-rouge">systemctl restart kubelet</code></li>
    </ol>
  </li>
</ol>

<p>At this point, <code class="language-plaintext highlighter-rouge">kubelet</code> will be crashing, waiting for <code class="language-plaintext highlighter-rouge">kubeadm</code> instructions.</p>

<h3 id="bootstrapping-the-cluster">Bootstrapping the Cluster</h3>

<p>The next step must be performed only on the <code class="language-plaintext highlighter-rouge">master</code> node. This will bootstrap
the cluster. Other nodes will join in another step.</p>

<p>To bootstrap the cluster, use <code class="language-plaintext highlighter-rouge">kubeadm</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm init <span class="nt">--pod-network-cidr</span><span class="o">=</span>10.244.0.0/16 <span class="nt">--control-plane-endpoint</span><span class="o">=</span>master.kube.vito.sh
</code></pre></div></div>

<blockquote>
  <p>The command may take a while and will output instructions on how to allow
non-root access to <code class="language-plaintext highlighter-rouge">kubectl</code> and how to join nodes to the cluster. Take note of
those instructions!</p>
</blockquote>

<p>Exit the root shell, and go back to your non-root user. Then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
<span class="nb">sudo cp</span> <span class="nt">-i</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config
</code></pre></div></div>

<p>Finally, run <code class="language-plaintext highlighter-rouge">kubectl get ns</code>. If you get a response, congratulations, you have
a working master node!</p>

<h3 id="setup-flannel">Setup Flannel</h3>

<p>Flannel is a CNI plugin that allows networking to work on your cluster. More
information can be found <a href="https://github.com/flannel-io/flannel">here</a>.</p>

<p>Installation is straightforward, requiring a single command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
</code></pre></div></div>

<p>Then you can use <code class="language-plaintext highlighter-rouge">watch kubectl get pods --all-namespaces</code> and wait for Flannel to bootup.</p>

<h2 id="setup-workers">Setup Workers</h2>

<p>Now, SSH into each of the workers and using information emitted by kubeadm, join
them to the cluster. The command will look like the following, and must be executed
with root privileges:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm <span class="nb">join </span>master.kube.vito.sh:6443 <span class="nt">--token</span> qn2gjj.e6y272oiqhqa2emu <span class="se">\</span>
    <span class="nt">--discovery-token-ca-cert-hash</span> sha256:401e68dbd991b30bcad71ed2cc67c03c5e970e60ac6eb21db43679104431e678
</code></pre></div></div>

<h2 id="label-workers">Label Workers</h2>

<p>Login to the <code class="language-plaintext highlighter-rouge">master</code> node again, and check all nodes are visible using <code class="language-plaintext highlighter-rouge">kubectl get nodes</code>. Notice that wokers have no role, meaning they won’t pick containers to run. Label them as workers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl label node k8s-worker1 node-role.kubernetes.io/worker<span class="o">=</span>worker
kubectl label node k8s-worker2 node-role.kubernetes.io/worker<span class="o">=</span>worker
kubectl label node k8s-worker3 node-role.kubernetes.io/worker<span class="o">=</span>worker
kubectl label node k8s-worker4 node-role.kubernetes.io/worker<span class="o">=</span>worker
</code></pre></div></div>

<h3 id="bootstrapping-the-cluster-part-2">Bootstrapping the Cluster: Part 2</h3>

<h3 id="install-metallb">Install MetalLB</h3>

<p>MetalLB allows bare-metal clusters to have ingresses and load balancers without
requiring a cloud-provider.</p>

<p>The first step to install it is to enable <code class="language-plaintext highlighter-rouge">strictARP</code> in <code class="language-plaintext highlighter-rouge">kube-proxy</code>. This can be done by running
the command below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl edit configmap kube-proxy -n kube-system
</code></pre></div></div>

<p>Locate the line <code class="language-plaintext highlighter-rouge">strictARP:</code> and change its value from <code class="language-plaintext highlighter-rouge">false</code> to <code class="language-plaintext highlighter-rouge">true</code>.</p>

<p>Installing it is also simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml
</code></pre></div></div>

<p>Finally, it needs to be configured in order to know which IPs it may use. In my case, the following is enough:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># metallb-ippool.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">metallb.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">IPAddressPool</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">default</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">metallb-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">addresses</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">10.0.11.50-10.0.11.254</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">metallb.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">L2Advertisement</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">default</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">metallb-system</span>
</code></pre></div></div>

<p>Apply the manifest using <code class="language-plaintext highlighter-rouge">kubectl apply</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> metallb-ippool.yaml
</code></pre></div></div>

<h2 id="install-k8s_gateway">Install k8s_gateway</h2>
<p>k8s_gateway exposes a DNS server that is able to resolve ingresses to nodes IPs.
Installation is made through helm:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add k8s_gateway https://ori-edge.github.io/k8s_gateway/
helm install exdns --set domain=k.vito.sh --set service.loadBalancerIP=10.0.11.53 k8s_gateway/k8s-gateway
</code></pre></div></div>

<p>I want to expose <code class="language-plaintext highlighter-rouge">10.0.11.53</code> as the DNS service. My upstream DNS server forwards requests to <code class="language-plaintext highlighter-rouge">k.vito.sh</code> to this IP.</p>

<h2 id="install-nginx-ingress">Install nginx-ingress</h2>

<p><code class="language-plaintext highlighter-rouge">nginx-ingress</code> is also installed through helm:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
</code></pre></div></div>

<p>Apply the chart using the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace
</code></pre></div></div>

<h3 id="test-the-ingress-and-networking">Test the ingress and networking</h3>

<p>The following manifest contains a deployment, service, and ingress that shows NGiNX’s default welcome message:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Namespace</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">test-ingress</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">test-ingress</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">nginx</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">nginx</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">nginx:latest</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">80</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-service</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">test-ingress</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-ingress</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">test-ingress</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">nginx"</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">test.k.vito.sh</span>
      <span class="na">http</span><span class="pi">:</span>
        <span class="na">paths</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
            <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
            <span class="na">backend</span><span class="pi">:</span>
              <span class="na">service</span><span class="pi">:</span>
                <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-service</span>
                <span class="na">port</span><span class="pi">:</span>
                  <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>Apply it using <code class="language-plaintext highlighter-rouge">kubectl apply -f test.yaml</code>. Eventually, the page will be
available over <code class="language-plaintext highlighter-rouge">test.k.vito.sh</code>.</p>

<h2 id="enabling-support-for-persistentvolumeclaims">Enabling support for PersistentVolumeClaims</h2>

<p>For this part, a new machine will be provisioned. I’ll use alpine as it is
ultra lightweight, performs well, and installs even faster. I’ll configure
it with 4GB RAM, 4 CPUs, and 100GB of disk.</p>

<p>Once the VM is provisioned, let’s configure NFS on it:</p>

<p>First, install required packages:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apk update <span class="o">&amp;&amp;</span> apk add nfs-utils vim
</code></pre></div></div>

<p>Create the data directory:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> /data
</code></pre></div></div>

<p>Update its owner to <code class="language-plaintext highlighter-rouge">nobody:nogroup</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chown </span>nobody:nogroup /data
</code></pre></div></div>

<p>And configure <code class="language-plaintext highlighter-rouge">/etc/exports</code> just like in any other Linux server. It is
important to notice that each node that will be given access to the NFS server
must be included in the <code class="language-plaintext highlighter-rouge">/etc/exports</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/data 10.0.11.1(rw,sync,no_subtree_check) 10.0.11.2(rw,sync,no_subtree_check) 10.0.11.3(rw,sync,no_subtree_check) 10.0.11.4(rw,sync,no_subtree_check)
</code></pre></div></div>

<p>After saving the file, reload settings:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>exportfs <span class="nt">-afv</span>
</code></pre></div></div>

<p>And ensure to start the NFS service on boot:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rc-update add nfs
rc-service nfs start
</code></pre></div></div>

<h3 id="preparing-workers">Preparing Workers</h3>

<p>Now, on each worker, install <code class="language-plaintext highlighter-rouge">nfs-common</code> through APT:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt install -y nfs-common
</code></pre></div></div>

<p>And finally, install the provisioner through Helm:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner
helm <span class="nb">install </span>nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner <span class="se">\</span>
  <span class="nt">--create-namespace</span> <span class="se">\</span>
  <span class="nt">--namespace</span> nfs-system <span class="se">\</span>
  <span class="nt">--set</span> nfs.server<span class="o">=</span>nfs.kube.vito.sh <span class="se">\</span>
  <span class="nt">--set</span> nfs.path<span class="o">=</span>/data
</code></pre></div></div>

<p>Then, try to create a PVC:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># pvc-test.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">pvc-test</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">ReadWriteMany</span>
  <span class="na">storageClassName</span><span class="pi">:</span> <span class="s">nfs-client</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">20Gi</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> pvc-test.yaml
</code></pre></div></div>

<p>Reading the PVC status, everything should be good:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get pvc
NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc-test   Bound    pvc-ff3b4825-3836-4661-9958-c2b8948135af   20Gi       RWX            nfs-client     &lt;unset&gt;                 7s
</code></pre></div></div>

<h2 id="tls--other-trinkets">TLS &amp; Other Trinkets</h2>

<p>What’s missing now is an Argo CD instance, and have automatic TLS for our
services. This will be covered in the future!</p>

<p>Happy K8sing!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I know there are so many posts on how to configure a bare-metal K8s cluster, but after studying a lot, here’s my take on it.]]></summary></entry></feed>