<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Jozo's blog]]></title><description><![CDATA[Python developer. I care about clean code, sane deployment and nice interface.]]></description><link>https://jozo.io</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 17:30:08 GMT</lastBuildDate><atom:link href="https://jozo.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Plus Addressing In Stalwart]]></title><description><![CDATA[What Is Plus Addressing?
Plus addressing (also known as sub-addressing) lets you receive messages into one email account from multiple sub-addresses, such as user+tag@example.org. For example, your main email might be user@example.org, but you can al...]]></description><link>https://jozo.io/plus-addressing-in-stalwart</link><guid isPermaLink="true">https://jozo.io/plus-addressing-in-stalwart</guid><category><![CDATA[stalwart]]></category><category><![CDATA[sieve]]></category><category><![CDATA[selfhost]]></category><category><![CDATA[Mail]]></category><dc:creator><![CDATA[Jozef Gáborík]]></dc:creator><pubDate>Sat, 13 Dec 2025 20:06:58 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-what-is-plus-addressing">What Is Plus Addressing?</h2>
<p>Plus addressing (also known as <a target="_blank" href="https://en.wikipedia.org/wiki/Email_address#Sub-addressing">sub-addressing</a>) lets you receive messages into one email account from multiple sub-addresses, such as <code>user+tag@example.org</code>. For example, your main email might be <code>user@example.org</code>, but you can also use <code>user+work@example.org</code> for work-related apps and still receive all messages in the same inbox. This way, you’ll know which messages are work-related based on the address used.</p>
<p>Another bonus is when all such messages are automatically moved to a sub-folder named after the used tag (in this case <code>work</code>).</p>
<h2 id="heading-how-to-achieve-it-in-stalwarthttpsstalwart">How To Achieve It In <a target="_blank" href="https://stalw.art/">Stalwart</a></h2>
<p>Good news — plus addressing works <a target="_blank" href="https://stalw.art/docs/mta/inbound/rcpt/#subaddressing">by default</a> in Stalwart except for the bonus case, when you want to also move messages into appropriate folders. To achieve it, we need Sieve scripts.</p>
<p>Your first idea might be to add Sieve scripts into System scripts or User scripts in the Stalwart admin web UI. This will not work because you can’t use the <code>fileinto</code> directive in those scripts. We need to use something else.</p>
<h3 id="heading-use-managesieve">Use ManageSieve</h3>
<p>ManageSieve is a protocol (<a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc5804">RFC 5804</a>) for users to add and manage their own Sieve scripts on the mail server. You need a ManageSieve client to do it. I recommend <a target="_blank" href="https://docs.kde.org/stable5/en/pim-sieve-editor/sieveeditor/introduction.html">SieveEditor</a>.</p>
<p>This Sieve script detects the “tag” part of the email address, creates a folder if necessary, and moves the message into the folder.</p>
<pre><code class="lang-python">require [<span class="hljs-string">"envelope"</span>, <span class="hljs-string">"fileinto"</span>, <span class="hljs-string">"mailbox"</span>, <span class="hljs-string">"subaddress"</span>, <span class="hljs-string">"variables"</span>];

<span class="hljs-comment"># Check if the mail recipient address has a tag (:detail)</span>
<span class="hljs-keyword">if</span> envelope :detail :matches <span class="hljs-string">"to"</span> <span class="hljs-string">"*"</span> {
  <span class="hljs-comment"># Create a variable `tag`, with the the captured `to` value normalized</span>
  set :lower <span class="hljs-string">"tag"</span> <span class="hljs-string">"${1}"</span>;

  <span class="hljs-comment"># Store the mail into a folder with the tag name:</span>
  <span class="hljs-keyword">if</span> mailboxexists <span class="hljs-string">"${tag}"</span> {
    fileinto <span class="hljs-string">"${tag}"</span>;
  } <span class="hljs-keyword">else</span> {
    fileinto :create <span class="hljs-string">"${tag}"</span>;
  }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[How to split traffic on GlobalProtect VPN on Mac OS]]></title><description><![CDATA[Updated on 2021-01-16
Recently the company I work for started to use Palo Alto's GlobalProtect as a solution for VPN. The solution works quite well but has 2 flaws by default that I don't like.
First is that the GlobalProtect agent (client) runs auto...]]></description><link>https://jozo.io/how-to-split-traffic-on-globalprotect-vpn-on-mac-os</link><guid isPermaLink="true">https://jozo.io/how-to-split-traffic-on-globalprotect-vpn-on-mac-os</guid><category><![CDATA[vpn]]></category><category><![CDATA[privacy]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Jozef Gáborík]]></dc:creator><pubDate>Sat, 16 Jan 2021 19:20:54 GMT</pubDate><content:encoded><![CDATA[<p><em>Updated on 2021-01-16</em></p>
<p>Recently the company I work for started to use Palo Alto's GlobalProtect as a solution for VPN. The solution works quite well but has 2 flaws by default that I don't like.</p>
<p>First is that the GlobalProtect agent (client) runs automatically after the operating system turns on and this behavior can't be changed in the settings. You can find a solution for it on <a target="_blank" href="http://richddean.com/post/147155656349/stopautostartglobalprotectvpn">other blogs</a>.</p>
<p>The second flaw is that it automatically sends <em>ALL</em> of my traffic through my company's VPN. I don't think this is beneficial for the company but most importantly it goes against my privacy. There is no need for the employer to know what goes on in my traffic.</p>
<p>This article describes:</p>
<ul>
<li><p>How to split traffic based on IP addresses</p>
</li>
<li><p>How to do traffic splitting automatically after the GlobalProtect agent connects to VPN</p>
</li>
</ul>
<p>I will only focus on Mac OS but similar steps can be taken also on other operating systems.</p>
<h2 id="heading-traffic-split-with-globalprotect">Traffic split with GlobalProtect</h2>
<p>When you connect to VPN with GlobalProtect, it creates a new network interface and edits the routing table so all our traffic is sent through this new network interface.</p>
<p>To solve this we need to remove a route created by GlobalProtect and then create a few new routes for only those IP addresses which we want to be directed through our VPN.</p>
<p>We implemented it in Python (based on this <a target="_blank" href="https://www.shadabahmed.com/blog/2013/08/11/split-tunneling-vpn-routing-table">blog post</a>). Save the script as split_vpn.py to your home folder. Edit the lists VPN_NETS and VPN_HOSTS based on your needs. Then you can run it every time you want to split traffic.</p>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python</span>
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> re
<span class="hljs-keyword">import</span> subprocess
<span class="hljs-keyword">import</span> sys

WIRELESS_INTERFACE = <span class="hljs-string">'en0'</span>    <span class="hljs-comment"># could be different on other systems</span>
TUNNEL_INTERFACE = <span class="hljs-string">'utun2'</span>

VPN_NETS = [
    <span class="hljs-string">'172.222'</span>,                <span class="hljs-comment"># subnets which should use VPN</span>
]

VPN_HOSTS = [
    <span class="hljs-string">'90.131.25.244'</span>,
    <span class="hljs-string">'90.131.25.240'</span>,        <span class="hljs-comment"># IP address which should use VPN</span>
]


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_tunnel_interface</span>():</span>
    <span class="hljs-comment"># Get last utun interface.</span>
    <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> reversed(subprocess.check_output([<span class="hljs-string">"ifconfig"</span>]).splitlines()):
        match = re.match(<span class="hljs-string">r"(utun\d)"</span>, line)
        <span class="hljs-keyword">if</span> match:
            <span class="hljs-keyword">return</span> match.groups(<span class="hljs-number">1</span>)[<span class="hljs-number">0</span>]


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">split_vpn_traffic</span>():</span>
    <span class="hljs-keyword">if</span> os.getuid() != <span class="hljs-number">0</span>:
        sys.exit(<span class="hljs-string">"Please, run this command with sudo."</span>)

    gateway = <span class="hljs-literal">None</span>
    tunnel_interface = get_tunnel_interface()
    out = subprocess.check_output((<span class="hljs-string">"netstat"</span>, <span class="hljs-string">"-nrf"</span>, <span class="hljs-string">"inet"</span>))
    routes = out.decode(<span class="hljs-string">"utf-8"</span>).split(<span class="hljs-string">"\n"</span>)[<span class="hljs-number">3</span>:]

    <span class="hljs-keyword">for</span> route <span class="hljs-keyword">in</span> routes:
        route = route.split()
        interface = route[<span class="hljs-number">3</span>]
        <span class="hljs-keyword">if</span> interface == WIRELESS_INTERFACE:
            gateway = route[<span class="hljs-number">1</span>]
            <span class="hljs-keyword">break</span>

    <span class="hljs-keyword">if</span> gateway <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        sys.exit(<span class="hljs-string">"Unable to determine VPN default gateway."</span>)

    print(<span class="hljs-string">"Resetting routes with gateway "</span> + gateway)
    subprocess.call(
        (<span class="hljs-string">"route"</span>, <span class="hljs-string">"-n"</span>, <span class="hljs-string">"delete"</span>, <span class="hljs-string">"default"</span>, <span class="hljs-string">"-ifscope"</span>, WIRELESS_INTERFACE)
    )
    subprocess.call(
        (<span class="hljs-string">"route"</span>, <span class="hljs-string">"-n"</span>, <span class="hljs-string">"delete"</span>, <span class="hljs-string">"-net"</span>, <span class="hljs-string">"default"</span>, <span class="hljs-string">"-iface"</span>, tunnel_interface)
    )
    subprocess.call((<span class="hljs-string">"route"</span>, <span class="hljs-string">"-n"</span>, <span class="hljs-string">"add"</span>, <span class="hljs-string">"-net"</span>, <span class="hljs-string">"default"</span>, gateway))

    print(<span class="hljs-string">"\nAdding routes for addresses which should go through VPN."</span>)
    <span class="hljs-keyword">for</span> addr <span class="hljs-keyword">in</span> VPN_NETS:
        subprocess.call(
            (<span class="hljs-string">"route"</span>, <span class="hljs-string">"-n"</span>, <span class="hljs-string">"add"</span>, <span class="hljs-string">"-net"</span>, addr, <span class="hljs-string">"-iface"</span>, tunnel_interface)
        )
    <span class="hljs-keyword">for</span> addr <span class="hljs-keyword">in</span> VPN_HOSTS:
        subprocess.call(
            (<span class="hljs-string">"route"</span>, <span class="hljs-string">"-n"</span>, <span class="hljs-string">"add"</span>, <span class="hljs-string">"-host"</span>, addr, <span class="hljs-string">"-iface"</span>, tunnel_interface)
        )


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    split_vpn_traffic()
</code></pre>
<h2 id="heading-automatic-traffic-split-after-connecting-to-vpn">Automatic traffic split after connecting to VPN</h2>
<p>Now that we have the script to split our traffic, we want it to run automatically after we connect to VPN with GlobalProtect. As it is stated in the <a target="_blank" href="https://www.paloaltonetworks.com/documentation/80/globalprotect/globalprotect-admin-guide/globalprotect-clients/deploy-agent-settings-transparently/deploy-agent-settings-to-mac-clients/deploy-scripts-using-the-mac-plist">documentation</a>, GlobalProtect agent can run commands before connecting, after connecting and before disconnecting.</p>
<p>Follow these steps to run the script after GlobalProtect agent connects to VPN:</p>
<ol>
<li><p>Disable and close GlobalProtect</p>
</li>
<li><p>Run <code>killall cfprefsd</code></p>
</li>
<li><p>Open in editor <code>/Library/Preferences/com.paloaltonetworks.GlobalProtect.settings.plist</code></p>
</li>
<li><p>Add to the section <code>/Palo Alto Networks/GlobalProtect/Settings/</code> following <em>(edit path based on your username)</em>:</p>
</li>
</ol>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>post-vpn-connect<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>command<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/Users/your_usename/post_vpn_connect.sh<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>context<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>admin<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
</code></pre>
<ol>
<li>Add this script to your home folder and save it as <code>post_vpn_connect.sh</code></li>
</ol>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
osascript -e <span class="hljs-string">'display notification "Start" with title "VPN traffic split"'</span>

DIR=<span class="hljs-string">"<span class="hljs-subst">$( cd <span class="hljs-string">"<span class="hljs-subst">$( dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span> )</span>"</span> &amp;&amp; pwd )</span>"</span>

<span class="hljs-variable">${DIR}</span>/split_vpn.py

country=`curl ifconfig.co/country`

osascript -e <span class="hljs-string">"display notification \"End. You country is <span class="hljs-variable">$country</span>\" with title \"VPN traffic split\""</span>
</code></pre>
<p>Now your traffic should be automatically split each time you connect to VPN with GlobalProtect. Nice!</p>
]]></content:encoded></item><item><title><![CDATA[Who knocks on my servers?]]></title><description><![CDATA[Weird domains and IP addresses in Django‘s exceptions „Invalid HTTP_HOST header“
A few months back I worked on a website for a local city festival. It’s a popular festival with lots of guests from different countries and a quite long history.
I migra...]]></description><link>https://jozo.io/who-knocks-on-my-servers</link><guid isPermaLink="true">https://jozo.io/who-knocks-on-my-servers</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Jozef Gáborík]]></dc:creator><pubDate>Sun, 07 Jun 2020 19:30:05 GMT</pubDate><content:encoded><![CDATA[<p><strong>Weird domains and IP addresses in Django‘s exceptions „Invalid HTTP_HOST header“</strong></p>
<p>A few months back I worked on a website for a local <a target="_blank" href="https://hanusovedni.sk/en/">city festival</a>. It’s a popular festival with lots of guests from different countries and a quite long history.</p>
<p>I migrated the site from WordPress to a brand new Wagtail solution. We have also moved from shared hosting to VPS. I wanted to progress quickly with development. So I launched a new version of the website as soon as possible with just uWSGI responding to all the HTTP traffic. Soon after the launch, I started to get weird errors in Sentry:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684265338327/fedd604a-39cf-47a4-8d54-294b8097699a.png" alt class="image--center mx-auto" /></p>
<p>Not truncated message from the error is:</p>
<pre><code class="lang-html">Invalid HTTP_HOST header: 'corpse.xyz'. You may need to add 'corpse.xyz' to ALLOWED_HOSTS.
</code></pre>
<p>The message was clear. Requests were coming to my server from a domain that is not allowed. How it can be? <em>“Maybe someone pointed his domain to my server’s IP address by mistake?”</em> was first thought in my head. But there were many of those requests from many different domains. And they tried to access weird URLs like:</p>
<pre><code class="lang-html">/wp-login.php
/wp/wp-login.php
/cgi-bin/test.sh
/cgi-bin/hello.sh
</code></pre>
<p>And also some of them came not from domain names but IP addresses. That was even weirder.</p>
<h2 id="heading-you-cant-trust-host-header">You can’t trust Host header</h2>
<p>After some time I found the reason behind this strange behavior. Domains and IP addresses I saw in Sentry or logs are from the HTTP header called “Host”. And <em>all</em> HTTP headers are set by the client. In other words, you can’t trust HTTP headers. Clients can set the header to whatever value they like. For example, this is how you can do it in Python with library <a target="_blank" href="https://requests.readthedocs.io">requests</a>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests

requests.get(<span class="hljs-string">"http://hanusovedni.sk"</span>, headers={<span class="hljs-string">"Host"</span>: <span class="hljs-string">"corpse.xyz"</span>})
</code></pre>
<p>Those weird requests were coming not from humans but from robots. There are a lot of robots that scan all IP addresses that exist on the Internet and are looking for vulnerabilities on websites. There are two groups of such robots:</p>
<ol>
<li><p>attackers who want to misuse vulnerabilities</p>
</li>
<li><p>white-hat organizations like The Shadowserver Foundation finding and reporting vulnerabilities</p>
</li>
</ol>
<p>Either way, you don't want your application to deal with them.</p>
<h2 id="heading-solution">Solution</h2>
<p>There is actually no real problem when you see this type of exception. It means Django is doing its job and filters malicious requests. But we want to filter out such requests as soon as possible. In most cases, it means filtering it at reverse proxy. In my case it‘s <a target="_blank" href="https://traefik.io">Traefik</a>. You can create rules for Treafik as labels on your services in <code>docker-compose.yml</code>. This rule will pass to Django only requests with the Host header set to my domain.</p>
<pre><code class="lang-html">traefik.http.routers.web-router.rule=Host(`hanusovedni.sk`, `www.hanusovedni.sk`)
</code></pre>
<p>The full definition of service from <code>docker-compose.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">${WEB_IMAGE}</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">secrets.env</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">DJANGO_SETTINGS_MODULE:</span> <span class="hljs-string">"hanusovedni.settings.production"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/www/static:/static_root</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/www/media:/media_root</span>
    <span class="hljs-attr">deploy:</span>
      <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
      <span class="hljs-attr">restart_policy:</span>
        <span class="hljs-attr">condition:</span> <span class="hljs-string">on-failure</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.enable=true"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.web-router.rule=Host(`hanusovedni.sk`, `www.hanusovedni.sk`)"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.services.web.loadbalancer.server.port=8000"</span>
</code></pre>
<h2 id="heading-what-can-an-attacker-do-with-the-host-header-anyway">What can an attacker do with the "Host" header anyway?</h2>
<p>Let me introduce you to the world of HTTP Host header attacks. Your website can be in danger only if you (or any code that you run) use the "Host" header. A typical example is about sending an email with a link to reset the password. Let's assume you build the message this way:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">build_message_for_password_reset</span>(<span class="hljs-params">request</span>):</span>
    token = <span class="hljs-string">"hash42"</span>   <span class="hljs-comment"># usually there will be some kind of hash</span>
    link = <span class="hljs-string">"http://"</span> + request.META[<span class="hljs-string">'HTTP_HOST'</span>] + <span class="hljs-string">"/reset-password/"</span> + token
    <span class="hljs-keyword">return</span> <span class="hljs-string">f"Click on this link to reset your password: <span class="hljs-subst">{link}</span>"</span>
</code></pre>
<p>If the attacker sends as the "Host" header his website "<a target="_blank" href="http://evil-web.com">evil-web.com</a>" the user will (potentially) click on this URL:</p>
<pre><code class="lang-html">http://evil-web.com/reset-password/hash42
</code></pre>
<p>At this point, the attacker can catch the token and reset the user's password and show the user whatever he wants.</p>
<p>Now you understand why it's important to correctly set <code>ALLOWED_HOSTS</code> in Django's settings. 😉</p>
]]></content:encoded></item></channel></rss>