<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Vanis</title>
    <link>https://vanis.sh/</link>
    <description>Wayland, networking &amp; systems programming — Vanis's blog</description>
    <atom:link href="https://vanis.sh/feed.xml" rel="self"/>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <language>en</language>
    <lastBuildDate>Sat, 29 Aug 2026 01:46:08 +0000</lastBuildDate>
    <item>
      <title>PPPoE on non performant routers</title>
      <link>https://vanis.sh/log/pppoe-on-non-performant-routers.html</link>
      <description>This log entry goes over PPPoE and its encapsulation.</description>
      <content:encoded><![CDATA[<p>
   Ever thought about why Internet Service Providers move away from PPPoE in favour of DHCP? Me too.
   PPPoE, or Point-to-Point Protocol over Ethernet, is used to encapsulate Point-to-Point frames inside of Ethernet frames.
   The Point-to-Point Protocol can be used over a variety of mediums, such as serial cables, phone lines, etc. For
   Ethernet use it needs encapsulating. Meaning every single packet requires extra work. That might not be an issue
   for slower links, but once the down/up is near 1000/1000, low end client hardware, which is most common,
   will start struggling and reducing throughput.
</p><p>
Good thing about PPPoE however is its nature of quick recovery. In a scenario where the ISP has two routers, one main and one backup,
and the main one requires an update with the backup serving all clients for the time being, a client running via DHCP will not know of 
the change some standard 30 minutes later, whereas PPPoE running client will know of the change within minutes thanks to PADI. One could
argue with DHCP FORCERENEW, but that is hardly supported on client hardware.
</p><p>
Some ISPs offer high speed connections (think 8000/8000), that is difficult to promise and deliver over PPPoE.
</p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/log/pppoe-on-non-performant-routers.html</guid>
      <pubDate>Sat, 29 Aug 2026 12:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Stateless DDoS protection via Vector Packet Processing</title>
      <link>https://vanis.sh/blog/stateless-ddos-protection-via-vector-packet-processing.html</link>
      <description>This blog post goes over building your own stateless Scrubbing Center, which can withstand various types of attacks preventing a denial of service for customers.</description>
      <content:encoded><![CDATA[<p>
    Criminal groups target Internet Service Providers with Distributed Denial of Service attacks. They often demand ransom to stop the attack and threaten to launch a more devastating one if the ransom is not paid.
</p><p>
    Traditional methods of combatting such attacks rely on stateful firewalls, which are prone to state table exhaustion, thereby letting malicious traffic through or causing a DoS themselves. This blog post goes over building your own stateless Scrubbing Center, which can withstand various types of attacks preventing a denial of service for customers.
</p><p>
<i>A Scrubbing center within of AS cannot solve link saturation that is happening outside of it. If the purchased upstream capacity is lower than the attack launched by the adversary, the upstream provider will drop traffic before it can ever be filtered, resulting in a DoS.</i>
</p><h2>Vector Packet Processing</h2><p>
    VPP is a Linux foundation backed project, part of the FD.io (Fast Data) project, providing the functionality of network switches and routers. The design of VPP is hardware and deployment (bare metal, VM, container) agnostic as it runs completely in user space, providing a kernel bypass.
</p><p>
    Practically, VPP takes a whole vector of packets and pushes it through a graph node, before pushing it onto another node. Remaining packet vectors get processed even faster, as the instruction cache learns from the first vector packet. This is opposed to scalar processing, which is the standard method of packet processing in Linux, where each of these packets are pushed through nodes individually.
</p><p>
    Without VPP, the process of receiving packets is as follows: <i>NIC receives a packet -&gt; calls an interrupt -&gt; CPU stops work -&gt; switches to kernel space -&gt; kernel copies the packet over -&gt; switches to user space.</i> This context switching consumes a massive amount of CPU cycles. With VPP: <i>NIC receives a packet -&gt; Direct Memory Access is used to write packets to user space RAM. Instead of having to wait for interrupts, it can process packets immediately.</i>
</p><h2>What are we protecting against</h2><p>
    The efficiency of a DDoS attack lies in amplification. The bigger the ratio between sent and received packet sizes, the less bandwidth the attacker needs to cause damage. This is often done through reflection and use of protocols which produce much larger packet replies than what the original incoming packet was.
</p><p>
    CLDAP requests can reach 70x amplification. This means that to generate a 1.4 Tbps attack, the attacker only needs 20 Gbps of traffic. However, CLDAP traffic can be cut off without causing DoS for many customers, so although it provides astronomical amplifications, in reality it does not cause much trouble, at least not in comparison with more standard protocols.
</p><p>
    TCP was designed to operate on unreliable networks, meaning a single SYN request can trigger several SYN-ACKs in rapid succession. There is not much amplification to be gained from singular TCP packets (up to 10%). However, attackers can achieve greater amplification by targeting IP space that is allocated, but is unoccupied. This prevents the source of the SYN-ACK from receiving a RST packet. The source will in turn send up to seven SYN-ACK packets before halting. That means that, in the worst case, a 40 byte request generated 308 bytes of packets arriving towards the target's infrastructure. That's an 7.7x amplification.
</p><h2>Scrubbing Center</h2><p>
    Network traffic scrubbing is one of the DDoS mitigation techniques. It consists of passing traffic through high capacity networks with "traffic scrubbing" filters.
</p><p>
    A Scrubbing center contains one or more scrubbing servers, which are dedicated machines that receives network traffic and attempts to filter good traffic from bad. Ideally, the scrubbing server will only forward non-DDoS packets.
</p><p>
    It can either be constructed locally, which has benefits of having almost no latency, or a cloud-based solution can be utilized. In any case, it is an off-ramp solution which is inactive if there is no attack and routes through it only activate when needed.
</p><figure>
<a href="https://vanis.sh/media/on-prem-sc-vs-cloud-sc.png">
<img alt="On-premise scrubbing center vs cloud-based scrubbing center." sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/on-prem-sc-vs-cloud-sc-small.webp" srcset="https://vanis.sh/media/on-prem-sc-vs-cloud-sc-tiny.webp 400w, https://vanis.sh/media/on-prem-sc-vs-cloud-sc-tiny.webp 800w, https://vanis.sh/media/on-prem-sc-vs-cloud-sc-small.webp 768w, https://vanis.sh/media/on-prem-sc-vs-cloud-sc-small.webp 1536w"/>
</a>
<figcaption>On-premise scrubbing center vs cloud-based scrubbing center.</figcaption>
</figure><p>
    Building an effective Scrubbing Center requires a ton of knowledge of the underlying protocols. It also requires hardware capable of digesting traffic volume of at least what the upstream provider grants, which can be hundred of Gigabits per second. If one does not have any DDoS protection whatsoever, they should weight in the possibility of opting for a cloud-based solution before they can develop their own local one.
</p><h2>Topology of the demo</h2><p>
    The demo consists of three interconnected debian servers with public IPv4 addresses.
</p><ul>
<li><code>rtr-1</code> : xxx.xx.x.177 (ISP Router)</li>
<li><code>rtr-2</code> : xxx.xx.x.178 (Scrubbing Center)</li>
<li><code>rtr-3</code> : xxx.xx.x.179 (Client)</li>
</ul><p>
    Each server has a network interface <code>ens18</code> with a public IP address and also:
</p><ul>
<li><code>rtr-1/ens19</code> (<code>10.0.12.1/30</code>) &lt;--&gt; <code>rtr-2/ens19</code> (<code>10.0.12.2/30</code>)</li>
<li><code>rtr-2/ens20</code> (<code>10.0.23.1/30</code>) &lt;--&gt; <code>rtr-3/ens19</code> (<code>10.0.23.2/30</code>)</li>
</ul><p>
    There is a web server running on <code>rtr-3</code> with the private ip of <code>10.100.100.1</code> accessible from <code>rtr-1</code> over a GRE tunnel going through the internet. Traffic is symmetrical without the use of the scrubbing center. However, in a filtering mode with the Scrubbing Center active, traffic to <code>rtr-3</code> goes through <code>rtr-2</code> where it gets processed and healthy bits are passed to <code>rtr-3</code> which replies through the tunnel, creating asymmetry.
</p><h2>Demonstration</h2><p>
    Let's setup our own scrubbing center now. If you would like to follow along in your own lab, make sure your topology resembles the one mentioned above. I will assume all the interfaces are configured and that <code>rtr-1</code> can access a webserver running on <code>rtr-3</code> with a single hop and that there is a way to get to <code>rtr-3</code> through <code>rtr-2</code>.
</p><h3>Prepare BGP for diversion of traffic</h3><p>
    First, we need a mechanism that dynamically pulls traffic away from the main link and into our Scrubbing Center during an attack. We will use the BIRD Internet Routing Daemon for this. Let's install it on both <code>rtr-1</code> and <code>rtr-2</code>:
</p><div>
<pre><code>apt install bird2</code></pre>
</div><h4>Configuring rtr-1 (ISP Router)</h4><p>
    This router receives routes from our Scrubbing Center. I only have one in this topology, but you can repeat the following configuration for as many as you'd like to simulate it better. Assign bigger metric to the routes in the kernel. This is so BGP gets priority when activated. <code>10.0.12.1</code> is the local IP address on an interface connecting this node to the Scrubbing Center and <code>10.0.12.2</code> is the IP address of the interface on the Scrubbing Center the node is connected with.
    <br/><code>/etc/bird/bird.conf</code> on <code>rtr-1</code>:
</p><div>
<pre><code>log syslog all;

router id 10.0.12.1;

protocol device { scan time 10; }
  
# Kernel route has a metric higher than 10
protocol kernel {
    ipv4 { import all; export all; };
    learn;
    metric 10;
}

protocol bgp scrubber {
    local 10.0.12.1 as 65001;
    neighbor 10.0.12.2 as 65002;
    ipv4 { 
        import all;
        export none;
    };
}</code></pre>
</div><h4>Configuring rtr-2 (Scrubbing Center)</h4><p>
    Here we define the static route <code>10.100.100.0/24</code> via <code>10.99.99.2</code> that will be advertised to the rest of the network during an attack. The IP address <code>10.99.99.2</code> acts as the gateway into VPP, which we will configure next.
    <br/><code>/etc/bird/bird.conf</code> on <code>rtr-2</code>:
</p><div>
<pre><code>log syslog all;

router id 10.0.12.2;

protocol device { scan time 10; }
protocol kernel { ipv4 { import all; export all; }; }

protocol static hijack {
    ipv4;
    route 10.100.100.0/24 via 10.99.99.2;
}

protocol bgp isp {
    local 10.0.12.2 as 65002;
    neighbor 10.0.12.1 as 65001;
    ipv4 {
        import none;
# none - diversion inactive
# all - diversion active
        export all; 
    };
}</code></pre>
</div><p>After pasting these configurations in, make sure to run <code>birdc configure</code>.</p><h3>Setting up VPP</h3><p>
    For the purpose of this demo, we will activate the scrubbing center manually and that by changing <code>"none"</code> to <code>"all"</code> in <code>rtr-2</code> BIRD config and watch the route change propagate to our ISP nodes. Notice the address <code>10.99.99.2</code>, it will be our scrubbing center running on <code>rtr-2</code>, to perform a kernel bypass and avoid having to work with scalar packets we must 
    avoid any bridges made by Linux interfaces, but for the purpose of this simplified demo we will set it up that way.</p><p>Install core packages needed for VPP to work.</p><div>
<pre><code>apt install vpp vpp-plugin-core vpp-plugin-dpdk</code></pre>
</div><p>
    VPP operates completely isolated bypassing the Linux kernel. To get data into it and back out, we must create dedicated interfaces.
    First, we hijack the physical network card that connects to the victim on <code>rtr-3</code> (in my case it is <code>ens20</code>) and assign it an IP address that resides in VPP:
</p><div>
<pre><code>vppctl create host-interface name ens20
vppctl set interface ip address host-ens20 10.0.23.1/30
vppctl set interface state host-ens20 up</code></pre>
</div><p>
    Next, we need to build a bridge between Linux (where BGP sends the diverted traffic) and VPP. We create a virtual <code>tap0</code> interface inside of VPP with the IP <code>10.99.99.2</code>:
</p><div>
<pre><code>vppctl create tap id 0 host-if-name vpp-scrub
vppctl set interface ip address tap0 10.99.99.2/30
vppctl set interface state tap0 up</code></pre>
</div><p>
    On the Linux side, we bring this bridge interface up and assign it an IP address in the same subnet:
</p><div>
<pre><code>ip link set vpp-scrub up
ip addr add 10.99.99.1/30 dev vpp-scrub</code></pre>
</div><p>The rules are now done, anything the Linux kernel routes to <code>10.99.99.2</code> will drop into VPP for filtering.</p><h4>Routing clean traffic</h4><p>
    VPP ignores Linux routing tables, so we need to manually tell it how to forward the cleaned traffic to the victim. We add a static route pointing out through the <code>host-ens20</code> interface:
</p><div>
<pre><code>vppctl ip route add 10.100.100.1/32 via 10.0.23.2 host-ens20</code></pre>
</div><p>
    So far we set up a network topology which can divert traffic through a Scrubbing Center (<code>rtr-2</code>) if needed. We have not yet added any rules into our Scrubbing Center, meaning the scrubbing does not scrub anything out of the network traffic. Let's do that now.
</p><h3>Simulating a single IP attack</h3><p>If you simulate a SYN-ACK flood on <code>rtr-1</code> targeting <code>rtr-3</code>:</p><div>
<pre><code>hping3 -S -A -p 80 --flood 10.100.100.1</code></pre>
</div><p>
    You should see a spike in CPU usage on <code>rtr-3</code> as an observation of the attack going through. If you then activate the following rule on <code>rtr-2</code> to statelessly drop SYN-ACK packets (TCP flags 18):
</p><div>
<pre><code>vppctl set acl-plugin acl "deny src 0.0.0.0/0 dst 0.0.0.0/0 proto 6 tcpflags 18 mask 18, permit src 0.0.0.0/0 dst 0.0.0.0/0"
vppctl set acl-plugin interface tap0 input acl 0</code></pre>
</div><p>Where the zero at the end is the index of the ACL. You can see your ACL rules with <code>vppctl show acl-plugin acl</code></p><p>You should see that the CPU usage on <code>rtr-3</code> drops. If you then run</p><div>
<pre><code>vppctl show int</code></pre>
</div><p>
    You should see <code>tap0</code> rx packets going sharply up, while <code>host-ens20</code> stays unchanged. If you then start a regular ping from <code>rtr-1</code> targeting <code>rtr-3</code>, while keeping the flood going, you should see tx packets under <code>host-ens20</code> going up. If you run <code>vppctl show errors</code> you will see exactly why the packets are being dropped.
</p><p>
<i>If the attack still goes through and packets are not being dropped, make sure there's <code>export all</code> in BIRD configuration on <code>rtr-2</code></i>
</p><h3>Simulating an IP range attack</h3><p>
    Targeting a single IP quickly triggers standard rate-limiting defenses and attackers know it. To bypass this, they spread the attack across the entire <code>/24</code> subnet.
    They will target IP space that is allocated, but is unoccupied (Dark space). Stateful firewall would try to open a connection state for every packet hitting these random IPs, quickly running out of RAM.
</p><p>Let's simulate this by targeting an IP that does not exist on <code>rtr-3</code>:</p><div>
<pre><code>hping3 -S -A -p 80 --flood 10.100.100.55</code></pre>
</div><p>
    VPP is completely stateless, it does not track connections or care if the destination IP exists. It matches the header against the ACL and drops it.
    After running <code>vppctl show errors</code>, you can see ACL deny packets counter going up on <code>rtr-2</code>. All the flood packets are instantly dropped.
</p><h2>Conclusion</h2><p>
    This single ACL rule blocks only a very specific kind of traffic and it is most likely not enough to prevent a sophisticated DDoS attack with attackers throwing random protocols around. You can dig into VPP's documentation and add more rules and filters, but it does not change the fact that setting up your own Scrubbing Center requires a lot of proactiveness to keep up with the variety of attack vectors, but even if you opt for a cloud-based protection, it never hurts to understand how one is build in practice.
</p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/blog/stateless-ddos-protection-via-vector-packet-processing.html</guid>
      <pubDate>Fri, 24 Jul 2026 12:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Frustration with the web</title>
      <link>https://vanis.sh/blog/frustration-with-the-web.html</link>
      <description>What a drag typing all of those numbers every time we want to visit a website, someone should think of a more efficient way for us humans to navigate the web. I know the numbers for my website by heart now, 49.12.41.13. Hold on, no numbers you say? But, how do you know which computers to contact?</description>
      <content:encoded><![CDATA[<p>
	What a drag typing all of those numbers every time we want to visit a website, someone should think of a more efficient
	way for us humans to navigate the web. I know the numbers for my website by heart now, 49.12.41.13. Hold on, no numbers you say?
	But, how do you know which computers to contact?
    </p><p>This is going to be a short post about authoritative DNS servers and about polymorphism.</p><hr/><p>The Domain Name System is the eminence of the internet without which it would come to a halt.</p><figure>
<a href="https://vanis.sh/media/domain-hierarchy-diagram.webp">
<img alt="Hierarchy of domains and domain names in DNS" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/domain-hierarchy-diagram.webp" srcset="https://vanis.sh/media/domain-hierarchy-diagram-tiny.webp 400w, https://vanis.sh/media/domain-hierarchy-diagram-tiny.webp 800w, https://vanis.sh/media/domain-hierarchy-diagram.webp 768w, https://vanis.sh/media/domain-hierarchy-diagram.webp 1536w"/>
</a>
<figcaption>Hierarchy of domains and domain names in DNS</figcaption>
</figure><p>
	The domain name system is a decentralized hierarchical database of records that allows
	the translation of hostnames to into IP addresses. Top level domains sit at
	the root of this hierarchy with these records scattered across the globe on thousands of servers. 
    </p><p>
	How does one make sense in it? When it comes to the practical resolving of DNS requests, they're
	processed top to bottom. DNS Resolver is a so called client side of the system responsible for initiating 
	queries that lead to a full translation. Resolvers check local cache first before sending the queries
	further if no match is found. When a request from a DNS client (like a web browser) is made, it tries
	to resolve it from a cache first. If it can't, it forwards it to an operating system level resolver 
	which does the same. If it can't resolve, the request is sent to a series of external DNS servers.
	The first of these is usually hosted by the ISP. This external DNS server sends the request
	over to the root DNS server, which is then able to forward to a top level domain DNS server. 
	The top level domain server then in turn request an authoritative DNS server containing
	information for the DNS zone it manages and connects the request with a record and corresponding IP address.
	The information is then returned to the client, completing the resolution. 
    </p><p>
	If we looked onto the internet through the lenses of a hacker who wants to demolish it, one of the fastest
	ways of achieving that would be for them to get rid of the domain root. DNS records are saved in a tree
	data structure, and without the root it ceases to work. To make the system more reliable, thirteen groups
	of root DNS servers exist, spread across hundreds of servers in various places around the globe, each
	assigned to one of these groups. DNS requests are then routed by anycast on top of BGP to the closest
	node which has the needed information. 
    </p><h2>DNS Authoritative servers</h2><p>
	DNS records were originally saved in a <code>hosts.txt</code> file. Similarly, zones were defined in zone files. 
	These files were hosted by a central authority from which they were downloaded by non-authoritative DNS servers
	to perform resolution. As the amount of records increased, these approaches were abandoned in favor of a distributed system.
    </p><p>
	Real implementations such as Knot DNS, a GPLv2 implementation of an authoritative-only server created and maintained by CZ.NIC,
	use complex algorithms and data structures for request resolution. I decided to create a naive version of a DNS server in C++.
	Why? University exams are soon, I need to study. Additionally, who doesn't like to learn about networking?
	It demonstrates some neat polymorphic patterns.   
    </p><p>
	Say we wanted our DNS server to store nestable zones and records (A, AAAA, CNAME, SPF). 
	It should also be able to print information about stored data.
	Super, we'll make a <code>Record</code> class and be done with it, no OOP needed right,... right? 
	Well, picture the class. If we wanted to store an "A" record, we would also have to allocate space
	for all the other types. If-else statements would take up the majority of the code. So, how will we do it? 
	<i>Polymorphically.</i> The ability in programming to present the same programming interface for differing
	underlying forms, so that different classes related by some common superclass can be used in place of that superclass.
    </p><p>
   	Let's try passing the following tests:	
    </p><div>
<pre><code>std::ostringstream oss;
Zone root_zone("&lt;ROOT ZONE&gt;");
Zone tld_edu_zone("edu");
Zone berkeley_zone("berkeley");
Zone berkeley_eng_zone("engineering");
Zone berkeley_chem_zone("chemistry");

assert(berkeley_eng_zone.add(
       Record_A("portal", IPv4("128.32.10.1"))) ==
       true);
assert(berkeley_eng_zone.add(Record_AAAA(
       "portal", IPv6("2607:f140:0:1::1"))) ==
       true);
assert(berkeley_eng_zone.add(Record_SPF("mail")
                 .add("ip4:128.32.0.0/16")
                 .add("ip6:2607:f140::/32")) ==
       true);

assert(berkeley_chem_zone.add(
       Record_A("www", IPv4("128.32.20.5"))) ==
       true);
assert(berkeley_chem_zone.add(Record_CNAME(
       "login", "auth.berkeley.edu.")) ==
       true);

assert(berkeley_zone.add(berkeley_eng_zone) == true);
assert(berkeley_zone.add(berkeley_chem_zone) == true);
assert(tld_edu_zone.add(berkeley_zone) == true);
assert(root_zone.add(tld_edu_zone) == true);

oss.str("");
oss &lt;&lt; root_zone;
assert(oss.str() ==
       "&lt;ROOT ZONE&gt;\n"
       "    edu\n"
       "        berkeley\n"
       "            engineering\n"
       "                portal A 128.32.10.1\n"
       "                portal AAAA 2607:f140:0:1::1\n"
       "                mail SPF ip4:128.32.0.0/16, "
       "ip6:2607:f140::/32\n"
       "            chemistry\n"
       "                www A 128.32.20.5\n"
       "                login CNAME auth.berkeley.edu.\n");

oss.str("");
oss &lt;&lt; tld_edu_zone;
assert(oss.str() ==
       "edu\n"
       "    berkeley\n"
       "        engineering\n"
       "            portal A 128.32.10.1\n"
       "            portal AAAA 2607:f140:0:1::1\n"
       "            mail SPF ip4:128.32.0.0/16, "
       "ip6:2607:f140::/32\n"
       "        chemistry\n"
       "            www A 128.32.20.5\n"
       "            login CNAME auth.berkeley.edu.\n");</code></pre>
</div><p>
	We create the root, TLD, berkeley, engineering and chemistry zones, nest them into each other so it makes sense. (with berkeley being 
	the parent of engineering and chemistry.) These zones are filled with records and subsequently printed. We'll start by
	defining what's common between all of our record types. In this simplified scenario it is the name and printing methods.
    </p><p>
	An abstract class that will serve as a parent in our polymorphic model:
    </p><code>
</code><div>
<pre><code>class IRecord {
public:
    IRecord(const std::string &amp;name) { _name = name; }
    virtual ~IRecord() = default;

    virtual void print(std::ostream &amp;os,
               const std::string &amp;prefix = "") const = 0;
    friend std::ostream &amp;operator&lt;&lt;(std::ostream &amp;os,
                    const IRecord &amp;record) {
        record.print(os, "");
        return os;
    }

protected:
    std::string _name;
};</code></pre>
</div><p>
	And we can now proceed to create four classes for our record types, each inheriting from IRecord and
	overriding the printing functionality:
    </p><div>
<pre><code>class Record_A : public IRecord {
public:
    Record_A(const std::string &amp;name, const IPv4 &amp;ip)
        : IRecord(name), _ip(ip) {}

    void print(std::ostream &amp;os,
           const std::string &amp;prefix = "") const override {
        os &lt;&lt; _name &lt;&lt; " A " &lt;&lt; _ip;
    }

private:
    IPv4 _ip;
};

class Record_AAAA : public IRecord {
public:
    Record_AAAA(const std::string &amp;name, const IPv6 &amp;ip)
        : IRecord(name), _ip(ip) {}

    void print(std::ostream &amp;os,
           const std::string &amp;prefix = "") const override {
        os &lt;&lt; _name &lt;&lt; " AAAA " &lt;&lt; _ip;
    }

private:
    IPv6 _ip;
};

class Record_CNAME : public IRecord {
      public:
    Record_CNAME(const std::string &amp;name, const std::string &amp;alias)
        : IRecord(name), _alias(alias) {}

    void print(std::ostream &amp;os,
           const std::string &amp;prefix = "") const override {
        os &lt;&lt; _name &lt;&lt; " CNAME " &lt;&lt; _alias;
    }

private:
    std::string _alias;
};

class Record_SPF : public IRecord {
public:
    Record_SPF(std::string name) : IRecord(name) {}
    Record_SPF &amp;add(const std::string &amp;item) {
        _items.push_back(item);
        // returns the current object to allow method chaining
        return *this;
    }

    void print(std::ostream &amp;os,
           const std::string &amp;prefix = "") const override {
        os &lt;&lt; _name &lt;&lt; " SPF";
        for (size_t i = 0; i &lt; _items.size(); ++i) {
            if (i == 0) {
                os &lt;&lt; " " &lt;&lt; _items[i];
            } else {
                os &lt;&lt; ", " &lt;&lt; _items[i];
            }
        }
    }

private:
    std::vector&lt;std::string&gt; _items;
};</code></pre>
</div><p>
	However, how will we store the zone? Remember that a zone can be added as a record to
	another zone. We'll use the composite pattern with our zone inheriting from the common
	superclass. This will allow us to create a tree like structure of zones:
</p><div>
<pre><code>class Zone : public IRecord {
public:
    Zone(std::string name) : IRecord(name) {}
    bool add(const IRecord &amp;record) {
        // have to call clone because we 1) don't know the type and 2)
        // cannot create an instance of an abstract class
        // also since this means that we are not storing a reference, but
        // a deep copy = no cycles with zones
        // Did not include the clone method to keep the code simple, but
        // it essentially simply returns a new object of itself through
        // the copy constructor
        _registrar.push_back(
            std::unique_ptr&lt;IRecord&gt;(record.clone()));
        return true;
    }
    void print(std::ostream &amp;os,
           const std::string &amp;prefix = "") const override {
        os &lt;&lt; _name;
        std::string newPrefix = prefix + "    ";
        for (size_t i = 0; i &lt; _registrar.size(); ++i) {
            os &lt;&lt; "\n" &lt;&lt; newPrefix;
            _registrar[i]-&gt;print(os, newPrefix);
        }
    }
    friend std::ostream &amp;operator&lt;&lt;(std::ostream &amp;os, const Zone &amp;zone) {
        zone.print(os, "");
        os &lt;&lt; "\n";
        return os;
    }
private:
    std::vector&lt;std::unique_ptr&lt;IRecord&gt;&gt; _registrar;
};</code></pre>
</div><p>
	And just like that, we've created a DNS server! 
</p><figure>
<a href="https://vanis.sh/media/zone-hierarchy-diagram.webp">
<img alt="Hierarchy of zones in DNS" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/zone-hierarchy-diagram.webp" srcset="https://vanis.sh/media/zone-hierarchy-diagram-tiny.webp 400w, https://vanis.sh/media/zone-hierarchy-diagram-tiny.webp 800w, https://vanis.sh/media/zone-hierarchy-diagram.webp 768w, https://vanis.sh/media/zone-hierarchy-diagram.webp 1536w"/>
</a>
<figcaption>Hierarchy of zones in DNS</figcaption>
</figure><p>
	All in all, we end up with a program that allows us to define zones in a 
	tree like structure, just like it is done in practice. What's next? 
	We could implement functionality to search for records, to delete them, 
	to manage exclusivity,.. and all of that should be relatively
	non-problematic thanks to the architecture chosen.
    </p><p>
	Perhaps I'll come back to this later and try to implement a super fast
	searching algorithm, but until that happens our server will keep things 
	to itself.
    </p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/blog/frustration-with-the-web.html</guid>
      <pubDate>Fri, 01 May 2026 12:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Ancient router reverse engineering</title>
      <link>https://vanis.sh/blog/ancient-router-reverse-engineering.html</link>
      <description>You used some sort of a router to view this page and if not, I have questions to ask. Ever wondered what the hardware consists of, is it perhaps an alien technology with little green gremlins routing the network traffic? Well, it looks rather green, but I do not see gremlins.</description>
      <content:encoded><![CDATA[<p>
      (No, I will not run Wayland on there.)
    </p><p>
      You used some sort of a router to view this
      page and if not, I have questions to ask. Ever wondered 
      what the hardware consists of, is it perhaps an alien technology 
      with little green gremlins routing the network traffic? Well, it looks 
      rather green, but I do not see gremlins.
    </p><hr/><p>
      This particular router has a reserved space in my heart 
      as it was this specific machine which allowed me to access 
      the internet for the first time all of these years ago. 
      Hence why I think it is only fair to peek under
      the lid, quite literally, instead of meanly throwing it out.
    </p><p>
    When the TL-WR543G became available, Atheros 
    <a href="https://www.wifi-insider.com/atheros/xr.htm">eXtended Range (XR)</a> 
    technology for maintaining connection with devices with low signal,
    together with a focus on client mode, differentiated 
    it from competition. Atheros released a white paper for its XR technology 
    back in April of 2004. It worked by improving sensibility to -105 dBm 
    and lowering bandwidth down to 256 Kbps if needed to preserve connections 
    instead of dropping devices. Interestingly though, Atheros does not mention 
    this technology anywhere in the spec sheet for their SOCs. Perhaps because it 
    would then not comply with the minimum WiFi bandwith of 1 Mbps required by the IEEE standard. 
    </p><p>
    There are reasons for <abbr title="Wrote in early 2026">modern</abbr> routers to
    hold a minimum sensitivity of around -95 dBm instead of going as weak as this router
    does. For one, router manufacturers do not
    have to deal with customers returning products in disappointment when network 
    traffic does not go through, even though it was said it would. 
    Reliability at such low decibel-milliwatts can not be well 
    guaranteed, so the connection is rather dropped and it is called a day. 
    Secondly, -95 dBm is way less than a modern day phone can listen for. 
    Smartphones cut out at around -85 dBm (eight times less),
    as their small antennas can physically not
    listen for such weak signals. Meaning it is quite a niche use-case and probably 
    better solved by adding an access-point. Nevertheless, back when this router released
    it was a hit.
    </p><h2>Specification</h2><p>
      With its declaration of conformity signed on the 24. of March 2007, the TL-WR543G v2
      is equipped with the Atheros AR2317 SOC processor and radio,
      Marvell 88E6063-RCJ1 switch for wired traffic, 
      ESMT M12L128168A memory,
      HST-2027DR / 1025DR Group-tek surge protectors, 
      serial port.
    </p><h2>Gallery:</h2><figure>
<a href="https://vanis.sh/media/WR543G-front.jpg">
<img alt="Front panel of the WR543G TP-LINK router" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/WR543G-front-small.webp" srcset="https://vanis.sh/media/WR543G-front-tiny.webp 400w, https://vanis.sh/media/WR543G-front-tiny.webp 800w, https://vanis.sh/media/WR543G-front-small.webp 768w, https://vanis.sh/media/WR543G-front-small.webp 1536w"/>
</a>
<figcaption>Front panel of the WR543G TP-LINK router</figcaption>
</figure><figure>
<a href="https://vanis.sh/media/WR543G-pcbfront.jpg">
<img alt="PCB front of the WR543G TP-LINK router" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/WR543G-pcbfront-small.webp" srcset="https://vanis.sh/media/WR543G-pcbfront-tiny.webp 400w, https://vanis.sh/media/WR543G-pcbfront-tiny.webp 800w, https://vanis.sh/media/WR543G-pcbfront-small.webp 768w, https://vanis.sh/media/WR543G-pcbfront-small.webp 1536w"/>
</a>
<figcaption>PCB front of the WR543G TP-LINK router</figcaption>
</figure><figure>
<a href="https://vanis.sh/media/hwlab-soldering-machine.jpg">
<img alt="WR543G TP-LINK with soldered serials" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/hwlab-soldering-machine-small.webp" srcset="https://vanis.sh/media/hwlab-soldering-machine-tiny.webp 400w, https://vanis.sh/media/hwlab-soldering-machine-tiny.webp 800w, https://vanis.sh/media/hwlab-soldering-machine-small.webp 768w, https://vanis.sh/media/hwlab-soldering-machine-small.webp 1536w"/>
</a>
<figcaption>WR543G TP-LINK with soldered serials</figcaption>
</figure><figure>
<a href="https://vanis.sh/media/WR543G-boot-menu.webp">
<img alt="WR543G TP-LINK boot menu" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/WR543G-boot-menu-small.webp" srcset="https://vanis.sh/media/WR543G-boot-menu-tiny.webp 400w, https://vanis.sh/media/WR543G-boot-menu-tiny.webp 800w, https://vanis.sh/media/WR543G-boot-menu-small.webp 768w, https://vanis.sh/media/WR543G-boot-menu-small.webp 1536w"/>
</a>
<figcaption>WR543G TP-LINK boot menu</figcaption>
</figure><p>
	To be continued... (Postponed due to exams)
    </p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/blog/ancient-router-reverse-engineering.html</guid>
      <pubDate>Mon, 23 Mar 2026 12:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Turning MacBook into a linux kiosk</title>
      <link>https://vanis.sh/blog/turning-my-macbook-into-a-linux-kiosk.html</link>
      <description>The unix philosophy speaks clearly - do a thing and do it well. What if there was a need to show a browser window, the time, weather, or even, given the current situation, stock prices. An embedded device with an external display would be a no-brainer choice for such project. However, could a…</description>
      <content:encoded><![CDATA[<p>
      The
      <a href="https://en.wikipedia.org/wiki/Unix_philosophy">unix philosophy</a>
      speaks clearly - do a thing and do it well. What if there was a need to
      show a browser window, the time, weather, or even, given the current
      situation, stock prices. An embedded device with an external display would
      be a no-brainer choice for such project.
    </p><p>However, could a modern MacBook running Linux be utilized instead?</p><p>
<a href="https://en.wikipedia.org/wiki/Asahi_Linux">Asahi Linux</a> is a
      project that ports the Linux kernel to Apple Silicon MacBooks via
      reverse-engineering the vastly undocumented hardware. The whole project
      has made invaluable contributions to freedom. The combined effort on all
      fronts allowed for a very stable Linux desktop experience on the newest
      hardware.
    </p><h2>The Linux Graphics stack</h2><p>
      There are topics to be explored before jumping into a project such as this
      one. The graphics stack being one of them - after all, that is what will
      make our kiosk display stuff on the screen! What follows is a bird's eye
      view of the Linux graphics stack.
    </p><h3>Kernel mode driver</h3><p>
      The kernel should provide free-as-in-freedom GPU drivers out of the box. A
      problem arises when there is hardware for which no drivers were written.
      Worse yet - undocumented Apple hardware.
    </p><p>
      Asahi Lina wrote the first kernel GPU driver for the Apple M-series and
      done so in Rust, thereby also claiming the spot for the first Linux GPU
      driver written in Rust. They made it possible to utilize the hardware and
      overall allowed us to understand the graphic accelerators more. All that
      is, at least in this very simplified scenario, left to do is to send
      instructions from the user-space so the accelerator knows what to do!
    </p><h3>User mode driver</h3><p>
      There are many graphics and compute APIs one can utilize to draw a game, a
      graphical application or heck, a whole
      <a href="https://vanis.sh/blog/playing-around-with-wayland.html">Window Manager</a>. Namely OpenGL, often utilized for desktops, and Vulkan. As it would be
      unreasonably difficult to implement support for every GPU into each of
      these APIs, and it would also disregard the unix philosophy, an
      intermediate layer,
      <a href="https://en.wikipedia.org/wiki/Mesa_(computer_graphics)">Mesa</a>,
      came to be. It contains various user-space drivers capturing buffers from
      the graphics APIs and translates them to calls the underlying kernel GPU
      driver understands.
    </p><p>
      The user-space GPU driver on Asahi Linux is called Honeykrisp, with
      <a href="https://en.wikipedia.org/wiki/Alyssa_Rosenzweig">Alyssa Rosenzweig</a>
      as the main contributor. This driver is
      <a href="https://www.khronos.org/conformance/adopters/conformant-products#submission_812">Khronos-recognized</a>

      and, at the time of writing this, is conformant with Vulkan 1.4, OpenGL
      4.6, OpenGL ES 3.2, and OpenCL 3.0. Honeykrisp is not entirely upstreamed
      to Mesa yet, that is why Asahi uses its own fork,
      <a href="https://docs.mesa3d.org/drivers/asahi.html">Asahi Mesa</a>, with
      all the patches present.
    </p><hr/><p>
      tinywl is the reference compositor implementation of wlroots, an
      abstraction library over Wayland. To test the ability of building parts of
      the Linux graphics stack locally, I decided to attempt to run tinywl
      through a locally built Mesa and wlroots. Building, apart from some minor
      hiccups, was quite pleasant as to be expected from Fedora, the
      distribution Asahi Linux is based on.
    </p><p>
      After the builds were finished, it was time to run tinywl as a session
      compositor nested inside of the already running Mutter. Using meson devenv
      pointing to locally built Asahi Mesa and running tinywl in the newly built
      wlroots directory. I heard the positive Honeykrisp Vulkan news and was
      excited to try the Vulkan renderer.
    </p><p>And tadaa ~ it did not work!</p><p>
<a href="https://docs.vulkan.org/spec/latest/appendices/extensions.html">Vulkan layers and extensions</a>
      allow for the insertion of additional functionality or structures to the
      base Vulkan spec. The Honeykrisp driver was missing an extension required
      by tinywl, specifically the <code>VK_EXT_queue_family_foreign</code>, to
      utilize the Vulkan renderer properly. I added the extensions locally and
      rebuilt.
    </p><p>
      Voilà, a wild tinywl window has appeared on the screen! I submitted a
      <a href="https://gitlab.freedesktop.org/mesa/mesa/-/issues/12904">feature request</a>
      in the Mesa issue tracker with Alyssa Rosenzweig swiftly adding the
      extension into Honeykrisp and marking the request as done.
    </p><p>
      With tinywl running as a session compositor I felt confident to run cage
      as a system compositor. After cloning and building cage I stumbled upon an
      issue - to run it via the Vulkan renderer, it required even more
      extensions to be enabled. Not a problem, I added them as before. Cage
      started just fine as a session compositor after this addition. However, as
      it had to start from a locally built version of Mesa running through meson
      devenv, which prohibits the use of root access, it was not possible to run
      it as a system compositor. To avoid installing a custom version of Asahi
      Mesa on the system or waiting for a new version to be packaged I decided
      to drop the Vulkan renderer and instead opted for OpenGL.
    </p><p>
      After creating a new entry in
      <code>`/usr/share/wayland-sessions`</code>, it was possible to launch cage
      running a terminal window, as shown below!
    </p><figure>
<a href="https://vanis.sh/media/asahi-linux-with-cage-compositor.webp">
<img alt="Asahi Linux with cage compositor" sizes="(max-width: 800px) 400px, 768px" src="https://vanis.sh/media/asahi-linux-with-cage-compositor-small.webp" srcset="https://vanis.sh/media/asahi-linux-with-cage-compositor-tiny.webp 400w, https://vanis.sh/media/asahi-linux-with-cage-compositor-tiny.webp 800w, https://vanis.sh/media/asahi-linux-with-cage-compositor-small.webp 768w, https://vanis.sh/media/asahi-linux-with-cage-compositor-small.webp 1536w"/>
</a>
<figcaption>Cage: a Wayland kiosk running on Asahi Linux</figcaption>
</figure><p>
      I waved the white flag on the Vulkan renderer, but other than that I
      consider this experiment to be a success. I am now able to experiment with
      Cage and the HoneyKrisp driver on Asahi Linux, perhaps submitting more
      feature requests making it possible to run Cage through Vulkan.
    </p><p>Until then, OpenGL for desktop it is!</p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/blog/turning-my-macbook-into-a-linux-kiosk.html</guid>
      <pubDate>Sat, 19 Apr 2025 12:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Playing around with Wayland</title>
      <link>https://vanis.sh/blog/playing-around-with-wayland.html</link>
      <description>Over the summer, I read a book on the Wayland protocol. Not a typical activity for a high school student to do, but there was something that seemed so intriguing about creating my own compositor.</description>
      <content:encoded><![CDATA[<p>
      Over the summer, I read a book on the
      <a href="https://wayland-book.com/">Wayland protocol</a>. Not a typical
      activity for a high school student to do, but there was something that
      seemed so intriguing about creating my own compositor.
    </p><p></p><p>
      Wayland is the superior replacement for the
      <a href="https://en.wikipedia.org/wiki/X_Window_System">X Windowing System</a>
      on Linux. X (or X11) was originally developed in 1984 and thus marks it's
      40 year anniversary this year. Wayland got initially released in 2008, and
      in contrarily with X, had desktop usage in mind from the beginning.
      Wayland, in comparision to X, provides developers a <i>simpler</i> API to
      work with. It is still not an easy feat, but, luckily for me, the amazing
      <a href="https://gitlab.freedesktop.org/wlroots">wlroots</a> abstraction
      library exists. It contains about 60 000 lines of boilerplate code that a
      Wayland compositor developer would have to write anyways. Instead of
      writting boilerplate, I could focus on the more interesting parts.
    </p><h2>Window manager / compositor</h2><p>
      These terms tend to be used interchagebly, but there is a difference. When
      one mentions the term "window manager", they typically mean the part that
      manages window placement on the screen and the logic behind it. In Wayland
      the responsibilty of a window manager is closelly tied to handling user
      input, server-side decorations etc.., Therefore it is called a compositor
      rather than a window manager, even though the window managing part is
      still there.
    </p><h2>The Cage compositor</h2><p>
      After reading through the most crucial parts of the Wayland book I decided
      to conduct research into compositors that are developed. I stumbled upon
      few. Namely tinywl, of which I read the whole implementation, but one
      stood out - the Wayland kiosk compositor Cage. A kiosk is, in essence, a
      normal compositor, but one that only displays a singular client at a time.
      I read through the code and was overwhelmed at first. However after
      rereading the main function a few times I felt like I could understand and
      visualize what was going on. Concepts learnt in the Wayland book, even
      though abstracted with wlroots, were still present. The code is by no
      means simple, at least not for someone who has only done a handful of DSA
      problems and no serious projects in C whatsoever.
    </p><p>
      I figured one of the more fun ways of exploring the codebase is to solve a
      real issue. Since a Wayland kiosk has a lot of usages, there's quite a lot
      of people creating repository issues asking for help or reporting bugs.
    </p><h2>Splash screen</h2><p>
      Imagine you are building a car infotainment system that has to display
      just one application. This is a perfect usecase for a kiosk compositor.
      There was someone that was doing exactly that asking for help in the
      repository's issues. They have an application that needs to be displayed
      reliably everytime it is needed. The computer running the application is
      regulary turned off and on again. When the computer boots,
      <a href="https://www.freedesktop.org/wiki/Software/Plymouth/">Plymouth</a>
      provides a flicker free loading experience. However, when it loads into
      the system and executes cage, the compositor instantly performs a modeset
      with black buffer, overriding the loader. Then, the loading application is
      shown and that's exactly what is not supposed to happen. There should be a
      way to prevent displaying the application which it is loading to provide a
      flicker free experience.
    </p><h3>Framebuffer, DRI, DRM, KMS</h3><p>
      Before diving into Wayland, I had no idea what these terms meant. In
      short, they are abstractions above the physical display hardware. Each of
      these components has a specific role in the process of displaying things
      onto the screen. Framebuffer allows software to display pixels by writting
      into designated memory spaces.
    </p><p>To be continued...</p>]]></content:encoded>
      <guid isPermaLink="true">https://vanis.sh/blog/playing-around-with-wayland.html</guid>
      <pubDate>Fri, 25 Oct 2024 12:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>
