Jekyll2024-01-28T20:04:44+00:00https://philippmundhenk.github.io/feed.xmlPhilipp Mundhenksome tech, some life. mostly tech.Philipp MundhenkSnappyMail Single Sign-On2024-01-28T00:00:00+00:002024-01-28T00:00:00+00:00https://philippmundhenk.github.io/SnappyMail_SSO<p>For many years, I had been using Rainloop as a fallback webmail client.
Since Rainloop is no longer maintained, I recently switched to <a href="https://snappymail.eu/">SnappyMail</a>.
Having spent lots of time to set up Traefik, Authelia, etc. I of course want to use single sign-on with as many services as possible.
When switching to SnappyMail, I picked up this topic one more time and solved it for me.</p>
<h2 id="challenge">Challenge</h2>
<p>As both Rainloop and the successor SnappyMail do not maintain any user database (and this is a good thing!), they rely on the IMAP server in the backend to perform the login.
In my case, the backend is Dovecot.
Thus, the credentials entered in SnappyMail get passed to Dovecot, which in turn uses the configured authentication backend (e.g., LDAP) to authenticate the user.
If we now want to use single sign-on with Traefik and Authelia, we log in with the credentials at Authelia, which might check with the same or a different authentication backend, but only passes on the username.
The password is of course not passed on and there is no way to retrieve it from the authentication backend.
At least there should not be.
Unfortunately, this means that single sign-on is incredibly difficult, as the required password for login can’t be passed to the IMAP server.
I stopped at this point quite a few years ago and only revisited this topic after migrating to SnappyMail.</p>
<h2 id="solution">Solution</h2>
<p>As it turns out, Dovecot has implemented the concept of a <a href="https://doc.dovecot.org/configuration_manual/authentication/master_users/">“Master User”</a>.
This is reminiscent of <code class="language-plaintext highlighter-rouge">sudo -u</code> and allows to log in any user with a central password.
While it has some security implementations (more on this later), this is perfect for what we are trying to achieve: Have SnappyMail use the master user to log in a user given in the HTTP header, passed on from Authelia, without requiring the user’s password.
While this does require some logic that has not been built up to this point, adding plugins to SnappyMail is trivial enough.
I thus spent a few hours developing some PHP and a bit of JavaScript to implement a SnappyMail plugin performing a master user login for a given HTTP remote_user.
I contributed this plugin upstream and is already available in the current release of SnappyMail under the name of <a href="https://github.com/the-djmaze/snappymail/tree/master/plugins/proxy-auth">Proxy Auth</a>.
You can install this directly from the admin panel of SnappyMail.</p>
<h2 id="features">Features</h2>
<ul>
<li>Master user login: The plugin allows to log in any user given via HTTP header at the IMAP backend.</li>
<li>Configurable HTTP header: The HTTP header containing the user name is configurable.</li>
<li>Proxy check: The calling IP address is checked against a given subnet to make sure the request comes from a reverse proxy server. Note that this does not offer perfect security, but it is a significant improvement.</li>
<li>Automatic login: By default, SnappyMail plugins are only available under a dedicated path. This plugin injects itself into the default login page and takes over the login, if the configured HTTP header is set.</li>
</ul>
<h2 id="example-configuration">Example Configuration</h2>
<p>Note: This is taken from the <a href="https://github.com/the-djmaze/snappymail/blob/master/plugins/proxy-auth/README.md">plugin README</a>. Refer to this README for the most up-to-date example.</p>
<p>The exact setup depends on your mailserver, reverse proxy, authentication solution, etc.
The following example is for Traefik with Authelia and Dovecot as mailserver but the plugin might also work for other combinations of services.</p>
<h3 id="snappymail">SnappyMail</h3>
<p>The following steps are require in SnappyMail:</p>
<ul>
<li>To open SnappyMail through a reverse proxy server (with redirect of authentication system), make sure to enable the correct secfetch policies: <code class="language-plaintext highlighter-rouge">mode=navigate,dest=document,site=cross-site,user=true;mode=navigate,dest=document,site=same-site,user=true</code> in the admin panel -> Config -> Security -> secfetch_allow.</li>
<li>Activate plugin in admin panel -> Extensions</li>
<li>Configure the plugin with the required data:
<ul>
<li>Master User Separator is dependent on Dovecot config (see below)</li>
<li>Master User is dependent on Dovecot config (see below)</li>
<li>Master User Password is dependent on Dovecot config (see below)</li>
<li>Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is “Remote-User”.</li>
<li>Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option!</li>
<li>When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet.</li>
<li>Automatic Login: Automatically logs in the user of user header is present (see below)</li>
</ul>
</li>
</ul>
<p>This concludes the setup of SnappyMail.</p>
<h3 id="dovecot">Dovecot</h3>
<p>In Dovecot, you need to enable Master User.
Enable <code class="language-plaintext highlighter-rouge">!include auth-master.conf.ext</code> in /etc/dovecot/conf.d/10-auth.conf.
The file /etc/dovecot/conf.d/auth-master.conf.ext should contain:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Authentication for master users. Included from auth.conf.
# By adding master=yes setting inside a passdb you make the passdb a list
# of "master users", who can log in as anyone else.
# <doc/wiki/Authentication.MasterUsers.txt>
# Example master user passdb using passwd-file. You can use any passdb though.
passdb {
driver = passwd-file
master = yes
args = /etc/dovecot/master-users
# Unless you're using PAM, you probably still want the destination user to
# be looked up from passdb that it really exists. pass=yes does that.
pass = yes
}
</code></pre></div></div>
<p>You then need to create a master user in /etc/dovecot/master-users:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>admin:PASSWORD::::::allow_nets=local,172.17.0.0/16
</code></pre></div></div>
<p>where the encrypted password <code class="language-plaintext highlighter-rouge">PASSWORD</code> can be created from a cleartext password with <code class="language-plaintext highlighter-rouge">doveadm pw -s CRYPT</code>.
It should start with <code class="language-plaintext highlighter-rouge">{CRYPT}</code>.
Username and password need to configured in the SnappyMail ProxyAuth plugin (see above).</p>
<p>You likely also want to limit the access by an IP address filter, e.g., to <code class="language-plaintext highlighter-rouge">local,172.17.0.0/16</code>, if you are running Postfix (<code class="language-plaintext highlighter-rouge">local</code>) and within a default Docker environment (<code class="language-plaintext highlighter-rouge">172.17.0.0/16</code>).
Otherwise, master user login (assuming password is known) is possible from every connectable system.
This is an unnecessary security risk.
For details see <a href="https://doc.dovecot.org/configuration_manual/authentication/allow_nets/">Dovecot documentation</a>.</p>
<p>Additionally, you need to set the master user separator in /etc/dovecot/conf.d/10-auth.conf, e.g., <code class="language-plaintext highlighter-rouge">auth_master_user_separator = *</code>.
The separator needs to be configured in the SnappyMail ProxyAuth plugin (see above).</p>
<h2 id="test">Test</h2>
<p>Once configured correctly, you should be able to access SnappyMail through your reverse proxy at <code class="language-plaintext highlighter-rouge">https://snappymail.tld/?ProxyAuth</code>.
If your reverse proxy provides the username in the configured header (e.g., Remote-User), you will automatically be logged in to your account.
If not, you will be redirected to the login page.</p>
<h2 id="automatic-login">Automatic Login</h2>
<p>By default, automatic login is activated.
Behind the scenes, this checks for the existence of the configured user header (through <code class="language-plaintext highlighter-rouge">/?UserHeaderSet</code>) and automatically redirects to <code class="language-plaintext highlighter-rouge">https://snappymail.tld/?ProxyAuth</code>, trying to log in the user.
Note that due to this implementation, logout is impossible, as once logged out, the user will automatically be logged in again.
The user is always considered logged in, as authentication is handled through reverse proxy and authentication system.</p>
<p>Auto login can be disabled in the plugin settings.
You can also change the logout link in admin panel -> Config -> custom_logout_link to the one of your authentication system, e.g., <code class="language-plaintext highlighter-rouge">https://auth.yourdomain.com/logout</code>.
In this case, you can log out from your overall system via SnappyMail.</p>
<h2 id="conclusion">Conclusion</h2>
<p>With the new Proxy Auth plugin, single sign-on with reverse proxy authentication and through HTTP header is now possible.
This was very fun excercise and once again a great opportunity to learn a few new skills and twists.
The plugin is available for SnappyMail from release v2.33.0.
Please let me know, if you are using this for other combinations of reverse proxy, authentication solution and IMAP server.
I believe this can work, especially with different authentication solutions, but have not tested it.
Please raise any issues in the SnappyMail repository, I will fix it there.</p>Philipp MundhenkFor many years, I had been using Rainloop as a fallback webmail client. Since Rainloop is no longer maintained, I recently switched to SnappyMail. Having spent lots of time to set up Traefik, Authelia, etc. I of course want to use single sign-on with as many services as possible. When switching to SnappyMail, I picked up this topic one more time and solved it for me.DIY Toothbrush Stand2023-11-05T00:00:00+00:002023-11-05T00:00:00+00:00https://philippmundhenk.github.io/Toothbrush_holder<p>This may be my shortest post in quite a while, but sometimes also small things bring large benefit.
I recently got a new electric toothbrush with a nice glass charger.
However, there was no stand or holder for the heads, leading to these flying around the bathroom shelves.
To improve this, I hacked together a minimal stand from leftover pieces.</p>
<h2 id="toothbrush-stand">Toothbrush Stand</h2>
<p>Frankly, I started out intending to make something a little nicer, but did not have the right tools at hand.
I am missing a router or similar.
Thus, after failing to achieve this, I decided to go for a smaller, more practical hack:
Starting from a leftover piece of wood, sanded nice and soft, and with rounded corners, I added a few nails.
The nails are just long enough that the toothbrush heads are not fully touching the wood, so that remaining water can escape, yet short enough so that there is no large gap.
The nails are spaced 2cm apart.
As this meant some of the nails are at the very limit of the size of the wood (i.e., pinching through the bottom), I added some flat foam feet.
Next time I oil the garden furniture, I might give this stand a bit of oil as well to make it handle remaining water a little better.
So whole effort is in the range of a few minutes.</p>
<h2 id="gallery">Gallery</h2>
<p><img src="/images/toothbrush_stand/equipped.jpg" alt="Toothbrush stand equipped" />
Toothbrush heads on stand.
Might be a few too many…</p>
<p><img src="/images/toothbrush_stand/empty.jpg" alt="Toothbrush stand empty" />
Better keep toothbrush heads on at all times, doesn’t like nice empty.</p>
<p><img src="/images/toothbrush_stand/bottom.jpg" alt="Toothbrush stand bottom" />
These are some simple foam feet.</p>Philipp MundhenkThis may be my shortest post in quite a while, but sometimes also small things bring large benefit. I recently got a new electric toothbrush with a nice glass charger. However, there was no stand or holder for the heads, leading to these flying around the bathroom shelves. To improve this, I hacked together a minimal stand from leftover pieces.Building a Homelab Shelf2023-10-06T00:00:00+00:002023-10-06T00:00:00+00:00https://philippmundhenk.github.io/Homelab_shelf<p>As shown in one of my <a href="https://www.mundhenk.org/homelab-review-2023/">last posts</a>, I operate a number of devices and services at home.
Until now, these had been distributed across my home, wherever there was power and network available.
This was not a very pleasant situation.
This, I decided to build a shelf for these (and future) devices.</p>
<h2 id="requirements">Requirements</h2>
<p>I intentionally did not fix on a 19” shelf, but instead start from scratch, thinking what I would need.
I wanted a shelf…</p>
<ul>
<li>…which is easy to move around, i.e., ideally on wheels and with few connections to the outside.</li>
<li>…which is large enough to house all my components with some space to spare, but not too large to obstruct too much.</li>
<li>…which is very flexible. I change my components often and want to be able to contain most of them in this shelf.</li>
<li>…which is easily accessible from at least front and back.</li>
<li>…with a display showing me the state of the system.</li>
<li>…which is as cheap as possible.</li>
</ul>
<h3 id="inspiration">Inspiration</h3>
<p>Before building, I pondered what I could do.
I quickly settled on the standard 19 inch rack not being very suitable for my purpose, as I have no 19 inch components and also don’t intend to buy any.
Furthermore, these are rather large.
While looking around for racks, I stumbled across 10 inch racks, which might have been a interesting option, however, except for one or two components, I would also just place everything on shelves or would need to build adapters.
Additionally, these are not very cheap.</p>
<p>There are, however, a number of excellent DIY server shelves out there.
In fact, too many to mention them all.
Thus, here is a list of those that most influenced this build:</p>
<ul>
<li>Jules Yap’s <a href="https://ikeahackers.net/2018/06/server-cabinet-diy-ikeahack.html">DIY Server Cabinet using IKEA parts</a> on Ikea hackers. While this is far too large, I really like the dark glas doors. This might have been what got me to get a glass door.</li>
<li>Haden James’s <a href="https://haydenjames.io/home-lab-beginners-guide-hardware/">12u Home lab rack</a>. Again, a rack, but very clean and with an amazing status display.</li>
<li>captain_yelland’s <a href="https://www.reddit.com/r/homelab/comments/mh4jj0/lots_of_ikea_lack_racks_over_here_but_what_to_do/">Ikea Besta rack</a>, especially for its cleanliness and non-rack components. I can learn a thing or two on cable management here.</li>
</ul>
<p>I then fairly quickly settled on an Ikea base.
With the proliferation of Ikea furniture, these are cheap to get used, and even if not I live a few minutes drive away from Ikea.
I first intended to get started with a Kallax shelf, inspired by these two:</p>
<ul>
<li>Teepo8080’s <a href="https://www.reddit.com/r/homelab/comments/ph3mxv/ikea_kallax_rack_part_2/">Kallax rack</a> which nicely combines rack monted components with non-rack components and additional shelving space.</li>
<li>FlomoN’s <a href="https://www.reddit.com/r/homelab/comments/pwfend/my_custombuilt_10inch_raspberry_pi_rack_in_ikea/">10-inch Raspberry Pi Kallax</a> with an amazing pull-out mechanism.</li>
</ul>
<p>However, I did not want to combine with other shelving and the available Kallax are not quite suitable for the size of the components I am looking at placing there (a potential future UPS would be too deep).
Furthermore, I did not find the size suitable as standalone shelf on wheels.</p>
<h2 id="components">Components</h2>
<p>I thus decided to got with a Besta shelf, partially triggered by the availability of used items and the general dimensions.
I managed to get a used white Besta 60x40x38cm and door for 20 Euros and an additional glass door for free in the area here.
I had to buy additional hinges for the glass door (20 Euros) and a shelf (8 Euros) directly from Ikea.
I also added the Bekant wheels (30 Euros), which nicely fit into the Besta feet inserts, though a bit long and expensive.
I would not necessarily recommend the wheels though, as I feel smaller wheels might look better, are cheaper, and also not difficult to mount.</p>
<p>Additionally, I decided to add a simple flap for cables (similar to <a href="https://www.amazon.de/SO-TECH%C2%AE-Kabeldurchf%C3%BChrung-Kabeldurchlass-Aluminium-eloxiert/dp/B00D1NUS64/ref=sr_1_13_sspa?__mk_de_DE=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=1XZOIH976PXPT&keywords=kabeldurchf%C3%BChrung&qid=1696585286&sprefix=kabel+durchf%C3%BChrun%2Caps%2C108&sr=8-13-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9tdGY&psc=1">this</a>) on the inside, which I got from a private seller (2 Euros) and, after some heat issues, a <a href="https://de.aliexpress.com/item/1005004412939382.html?spm=a2g0n.productlist.0.0.649665638KAP5T&browser_id=2eada12451e447d98b3a943882b7543c&aff_platform=msite&m_page_id=pcgyxrqcawimupkw18a1e87f723524c864b1ae03f9&gclid=&pdp_npi=4%40dis%21EUR%215.19%210.46%21%21%2140.33%21%21%402145288516927293417358389d075f%2112000029093848733%21sea%21DE%210%21A&algo_pvid=c0da01e3-cc40-4fc5-8c15-8e3f189f4c6e">grille</a> from Aliexpress (about 5 Euros).</p>
<p>For the display, I decided to <a href="https://www.mundhenk.org/kapps/">once again use a Kindle</a>, but this time nicely place it in an Ikea Ribba 13x18cm frame (2 Euros).</p>
<h2 id="building">Building</h2>
<p>Since I bought the shelf used, it was already built.
I did remove the back however and install the door from the front to the back side, so that the glass door could be mounted on the front.
This is fairly trivial, as the Besta shelves are symmetrical and the pre-drilled holes are already available on the back.</p>
<p>More troublesome was the integration of cable flap and grille.
However, as Ikea furniture is mostly cardboard and pressed wood, this is also not terribly difficult: The furniture can be cut with a simple carpet knife and some strength.
When installing, make sure to avoid cutting too close to the corners of any piece of furniture, as the corners are usually strengthened by wood on the inside.
You can determine the rough size by simply knocking and comparing the sound differences.
<img src="/images/homelab_shelf/cardboard.jpg" alt="Ikea cardboard furniture" /></p>
<p>For the display, I had to modify the Ribba frame by drilling space for the USB cable.
I modified the USB cable by removing a lot of the plastic covering to make it smaller and fit the frame.
Furthermore, I drilled a hole to be able to use the on/off switch of the Kindle while in the frame.
I would not recommend this, as the hole is a little too visible for my taste and does not function very well.
As the clamps holding the back of the frame in place are meant for pictures and not rather thick Kindles, I additionally placed new staples on the inside of the frame.
These hold the Kindle in place.
You may also use nails or similar here.</p>
<h2 id="gallery">Gallery</h2>
<p><img src="/images/homelab_shelf/full.JPG" alt="Front full" />
A view from the front.</p>
<p><img src="/images/homelab_shelf/display.JPG" alt="Status display" />
The status display, showing the system status from Uptime Kuma and the temperatures of two devices, collected via Glances and rendered in Home Assistant.</p>
<p><img src="/images/homelab_shelf/content.JPG" alt="Front content" />
The content of the shelf: A Netgear GS108T-300PES running OpenWRT (thus no LEDs), FritzBox (current router), Fujitsu S920 (future router), Brother label printer, Synology Diskstation, Western Digital USB Drive, Intel NUC.
See <a href="https://www.mundhenk.org/homelab-review-2023/">Homelab Review 2023</a> for details.
I definitely need to improve my cable management.</p>
<p><img src="/images/homelab_shelf/rear_top.JPG" alt="Top rear" />
After operating the shelf for a few days with closed doors, I started having network issues.
As it turns out, the older Netgear switch I was operating then did not take the temperatures very well.
There was just too little airflow.
I thus decided to add this grille for ventilation.
For now, I will keep this passive.
If required later, the grille and cutout are exactly big enough to place three 120mm fans.
This should alleviate any remaining heat issues.</p>
<p><img src="/images/homelab_shelf/cable_flap.jpg" alt="Cable flap" />
At the rear bottom, this where power and Ethernet are coming in.</p>
<p><img src="/images/homelab_shelf/rear.JPG" alt="Rear door" />
The rear door makes it easy to access cabling, especially power.
And yes, cable management is not any better at the back.</p>
<h2 id="software">Software</h2>
<h3 id="status-display">Status Display</h3>
<p>Currently, the shelf is not very smart.
I use some software to get temperature measurements though.
Here, I use Glances on multiple devices, which is pulled by Home Assistant.
There, I have some dedicated tabs to display the status of my servers.
These tabs also include information from Uptime Kuma, pulled through the <a href="https://github.com/meichthys/uptime_kuma">Uptime Kuma HACS integration</a>.
These pages are turned into grayscale PNGs with <a href="https://github.com/sibbl/hass-lovelace-kindle-screensaver">Home Assistant Lovelace Kindle Screensaver</a> by sibbl and served via the integrated webserver.
Here is an example docker-compose for two pages:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.8"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">sibbl/hass-lovelace-kindle-screensaver:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">HA_BASE_URL=http://homeassistant.tld</span>
<span class="pi">-</span> <span class="s">HA_SCREENSHOT_URL=/lovelace/servers?kiosk</span>
<span class="pi">-</span> <span class="s">HA_SCREENSHOT_URL_2=/lovelace/servers_mem?kiosk</span>
<span class="pi">-</span> <span class="s">HA_ACCESS_TOKEN=REDACTED</span>
<span class="pi">-</span> <span class="s">CRON_JOB=* * * * *</span>
<span class="pi">-</span> <span class="s">RENDERING_TIMEOUT=30000</span>
<span class="pi">-</span> <span class="s">RENDERING_DELAY=0</span>
<span class="pi">-</span> <span class="s">RENDERING_SCREEN_HEIGHT=800</span>
<span class="pi">-</span> <span class="s">RENDERING_SCREEN_WIDTH=600</span>
<span class="pi">-</span> <span class="s">GRAYSCALE_DEPTH=8</span>
<span class="pi">-</span> <span class="s">OUTPUT_PATH=/output/overview.png</span>
<span class="pi">-</span> <span class="s">OUTPUT_PATH_2=/output/memory.png</span>
<span class="pi">-</span> <span class="s">LANGUAGE=en</span>
<span class="pi">-</span> <span class="s">ROTATION=90</span>
<span class="pi">-</span> <span class="s">SCALING=1</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">44466:5000</span>
<span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">/data_dir/ha_screenshot/output/:/output</span>
</code></pre></div></div>
<p>I the run a small shell script on my jailbroken Kindle (<code class="language-plaintext highlighter-rouge">/mnt/us/status_display/run.sh</code>):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
mntroot rw
lipc-set-prop com.lab126.powerd preventScreenSaver 1
lipc-set-prop com.lab126.pillow disableEnablePillow disable
<span class="k">while </span><span class="nb">true
</span><span class="k">do
</span><span class="nb">rm </span>overview.png
<span class="k">if </span>wget http://192.168.1.123:44466/1 <span class="nt">-O</span> overview.png<span class="p">;</span> <span class="k">then
</span>eips <span class="nt">-c</span>
eips <span class="nt">-g</span> overview.png
<span class="nb">sleep </span>60
<span class="k">else
</span>eips <span class="nt">-c</span>
<span class="k">fi
</span><span class="nb">rm </span>memory.png
<span class="k">if </span>wget http://192.168.1.123:44466/2 <span class="nt">-O</span> memory.png<span class="p">;</span> <span class="k">then
</span>eips <span class="nt">-c</span>
eips <span class="nt">-g</span> memory.png
<span class="nb">sleep </span>60
<span class="k">else
</span>eips <span class="nt">-c</span>
<span class="k">fi
done</span>
<span class="o">}</span> | <span class="nb">tee</span> <span class="o">></span> log.log 2>&1
</code></pre></div></div>
<p>which I start automatically (<code class="language-plaintext highlighter-rouge">/etc/upstart/status.conf</code>):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#kate: syntax bash;
description "Status display"
start on started poll_daemons and started kb and started pillow and started acxmgrd and started cmd and started lab126 and started audio
script
su root -c /mnt/us/status_display/run.sh
return 0
end script
post-stop script
return 0
end script
</code></pre></div></div>
<h3 id="vlans">VLANs</h3>
<p>I did not manage to move all components into this shelf.
We have a DSL internet connection, which is terminated at a telco-controlled FritzBox and some Cat 7 wiring in the house.
The FritzBox, as well as a switch for the Cat 7 wiring and the Ethernet connection to my smartmeter read-out, remain external.
To avoid having to run too many (i.e., two) Ethernet wires to my new homelab shelf (one for internet & for home network), I introduced VLANs.
One VLAN is operating between the telco-controlled FritzBox and my FritzBox and the other VLAN is covering the rest of my internal network.
Both the GS108E outside the shelf, as well as the GS108T inside the shelf support VLANs.
I trunk the VLANs across the connection between the two switches, thus only requiring one Ethernet cable running to the shelf.</p>
<p>Note that this is not ideal, as of course the bandwidth is now shared.
This means, that I can’t saturate my internet connection and the connection to e.g., my DiskStation from within the home, as the sum would be above just above 1 GBit/s.
This situation happens rarely to never, so I can live with this compromise.
The alternative would have been a few hundred Euros of additional spending for 2.5 GBit/s switches with VLAN support.
I was not willing to shell out this amount of money for a seldom, minor inconvenience.
This is a potential point to upgrade in the not too distant future, though.</p>
<p>I was for a short amount of time considering to also use Powerline Communication to get rid of the last Ethernet cable, but decided against it.
The data rates and stability of the devices seems to be insufficient for the backbone of my IT infrastructure.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This project was a lot of fun and adds quite a bit of value to my homelab.
I now have some proper storage, which is flexible enough for frequent adaptions.
While not quite as standardized (and thus clean) as many 19 or 10 inch racks, it does fairly neatly pack all components into an easily movable, accessible, yet smallish rack.</p>
<h3 id="improvements">Improvements</h3>
<p>Of course, this is not perfect, there are always improvements possible:</p>
<ul>
<li>At the top of the list is cable management. The downside of having all components neatly stored in a box is the lack of cable chaos visibility, leading me to push this out further and further into the future.</li>
<li>The passepartout of the status display is quickly handmade, and it shows. I might order a proper made to measure one here.</li>
<li>Once there are additional components, heat might be an issue again. In that case, I intend to install up to three 120mm fans under the top grille.</li>
<li>The fans might also take care of the rather unsightly view into the shelf from the top. The grille allows a lot more view (and thus also dust) into the shelf than I had expected and I might cover this a little more.</li>
<li>I don’t quite like the way the USB cable of the Kindle is currently running through the grille and might add an additional hole for cables at the top. This would also allow to easily access e.g., cabled Ethernet or power when sitting at the shelf debugging.</li>
<li>I might need to add a few brackets for stability. With the rear wall exchanged for a door, the shelf is not very stable and leans slightly left or right, especially when the heavy glass door shifts the cener of gravity. This should be easily resolvable with a few angle brackets on the inside.</li>
<li>Some fixed mounted power outlets inside would be nice. I may or may not add them later on.</li>
<li>To judge temperature a little better, I might install one or two temperature sensors in the shelf.</li>
<li>Additionally, I might add a door opening sensor, as the kids play area is not very far from the shelf.</li>
<li>At some point, I might shorten the threads of the wheels, as they anyway don’t screw in fully, but this is really at the bottom of the list.</li>
</ul>Philipp MundhenkAs shown in one of my last posts, I operate a number of devices and services at home. Until now, these had been distributed across my home, wherever there was power and network available. This was not a very pleasant situation. This, I decided to build a shelf for these (and future) devices.Docker Networks & IP Address Ranges2023-09-03T00:00:00+00:002023-09-03T00:00:00+00:00https://philippmundhenk.github.io/Docker_IP_addresses<p>As shown in my <a href="https://www.mundhenk.org/homelab-review-2023/">last post</a>, I run a number of Docker containers.
For most of these, I let Docker create a default network.
However, by default Docker is rather wasteful with its address space, leading to conflicts in my networks.
Once identified, resolving this is straightforward though.</p>
<h2 id="background">Background</h2>
<p>I use three address ranges in my networks:</p>
<ul>
<li>192.168.0.0/16 is split into subnets and used for physical devices and VMs</li>
<li>10.0.0.0/8 is used for Wireguard clients</li>
<li>172.16.0.0/12 is used for Docker containers</li>
</ul>
<p>While this is of course a wasteful configuration in itself, as I have less than a handful of Wireguard clients which share the largest address space, this works fine as this is generally far over provisioned also in all other areas.</p>
<p>However, as Docker is by default splitting its address space into /20 size nets, leading to 12 bits, or 4096 hosts per net.
As most of my networks are having no more than two or three hosts, with the exception of the Traefik network, this is rather large.</p>
<h2 id="problem">Problem</h2>
<p>While this would generally be not much of a problem, due to the number of stacks I am running the address space is exhausted rather fast.
When this happens, Docker starts using the 192.168.0.0/16 address space in the same size chunks as above.
This leads to overlapping address spaces being assigned to physical devices and Docker containers.</p>
<p>In my case, funny enough, my Uptime Kuma instance was affected by the issue, leading to my off-site location not being pingeable from there, while being reachable perfectly fine from all other devices in the network.
When logging into the container and trying to ping a device in the other location, I received an unreachability response from the Docker gateway of the Docker network.</p>
<h2 id="solution">Solution</h2>
<p>The solution to this is fairly simple: Reconfigure the Docker daemon to assign smaller and selected, i.e. not otherwise assigned address spaces.</p>
<p>This is possible via <code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code> (create if not existing), like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"default-address-pools":
[
{
"base":"172.16.0.0/12",
"size":24
}
]
}
</code></pre></div></div>
<p>This uses /24 networks, which still leaves you with 256 hosts per network, but allows you 4096 networks, instead of 256 by default.
You may of course also define additional address ranges here, if you require more networks, e.g. of the 192.168.0.0/16 space, leaving out the addresses already in use.</p>
<p>After configuring this, make sure to restart the Docker daemon, e.g. via <code class="language-plaintext highlighter-rouge">systemctl restart docker</code>.
This is likely not sufficient though, if you already have existing networks. You will need to recreate these to take advantage of the smaller address spaces.
I simply restarted all my Docker Compose stacks, which recreated the networks, as well.
I did not have any other networks defined.</p>
<p>Congratulations! You are now saving 3840 IP reservations per network, or can create 16 networks for every default network.
So for the above address space, you now get 4096 networks instead of the earlier 256 networks.
This should be sufficient for most homelabs…</p>Philipp MundhenkAs shown in my last post, I run a number of Docker containers. For most of these, I let Docker create a default network. However, by default Docker is rather wasteful with its address space, leading to conflicts in my networks. Once identified, resolving this is straightforward though.Homelab Review 20232023-08-28T00:00:00+00:002023-08-28T00:00:00+00:00https://philippmundhenk.github.io/homelab-review-2023<p>I have been getting multiple requests recently to give an overview over my homelab.
I will do this in this article.
I will include some of the history, but ignore many of the experiments I tried and failed over the years (Banana Pi, RPi-based GlusterFS, easily scalable Kubernetes on RPis, Docker on ARMv6, etc.).</p>
<h2 id="history">History</h2>
<p>While my current homelab is comparably large, this has not always been the case.
To show that everyone starts small and a big step of achieving a goal is getting started, I decided to include a bit of history.</p>
<h3 id="pre-2009">pre 2009</h3>
<p>I started rather late into the world of computers. The first computer we had at home was a Pentium 2 with Windows 95. We did get a 56k dial-up internet connection some time in the 2000s. During this time, my main use for a computer was of course gaming. Through this and the according LAN parties, I gathered first experience with networking. I have never used token ring much, but mostly started on Ethernert. Configurations where finicky and we still used Ethernet hubs, rather than switches.</p>
<p>Around that time, USB became popular and after some USB thumb drives, I managed to get a rather cheap (for the standards of a high-school student) harddrive enclosure which also had an Ethernet port. I didn’t expect too much from it, but since it wasn’t too much more expensive than a standard (branded) USB enclosure, I thought I’d give it a try.</p>
<p>This little silver drive from a no-name manufacturer changed my life more than I would have ever expected.</p>
<p>It contained a simple FTP and SMB server, but instilled in me a mindset of sharing data across a network, rather than storing everything locally and moving around via USB thumb drives.</p>
<h3 id="2009-2014">2009-2014</h3>
<p>While this drive got me rather far in terms of network backups and file storage, it of course had quite some limits.</p>
<p>Around the time when I started studying in 2007, I was considering upgrading to something with a little more power (but still on a student budget). Multiple friends built their own servers at the time, but I was not very keen on the power consumption, nor the price, or effort. I was not sure if I had the knowledge and capabilities of building a stable system to rely on with my data at the time.</p>
<p>During that time I discovered a small Taiwanese manufacturer of network storage enclosures that seemed to be a good option, albeit not particularly cheap. However, they seemed very responsive, with the techs being available on the forums and feature requests by the community being implemented rather quickly.
This company is still around and has gotten rather famous in this space. It is called Synology. I settled on the lowest end DiskStation at the time, a DS210j.</p>
<p>This device was strongly limited in terms of RAM and CPU, but was one option I could afford. I equipped it with two 1.5 TB Western Digital drives (preceeding the Red series) in a RAID 1 configuration. This set me back quite a bit as a student, but gave me a stable basis for at least data sharing for quite some time.</p>
<p>Later, I experimented with adding additional services natively, such as the Zimbra groupware, but usually updates where troublesome then, so I mostly stick with official Synology packages later on and experimented with external servers going forward.</p>
<h3 id="2014-2017">2014-2017</h3>
<p>After multiple detours, I finally set up my own mailserver on a Raspberry Pi in 2014. While very outdated, the article on this is <a href="https://www.mundhenk.org/rpi-mailserver/">still available on my website</a>.</p>
<p>This was the first additional compute unit, and allowed me to focus the DS on storage.</p>
<p>During my time abroad and traveling a lot, I was very happy to have a home base with file storage and mail server, available via the IPSec VPN of a FritzBox router (see also <a href="https://www.mundhenk.org/fritzbox-openwrt-vpn/">here</a>).</p>
<p>Later, I extended this RPi with the CardDAV/CalDAV server Radicale, allowing me to sync calendars, contacts and tasks.</p>
<h3 id="post-2017">post 2017</h3>
<p>Over the years, the DS210j unfortunately got so slow that even just using the web interface was troublesome. Not really surprising, considering the small RAM on the ~j models. It could basically only be used for file storage. Since I got mt first job post-PhD in 2016, I decided to splurge a bit and get a DS718+, complete with additional 4 GB RAM (non-Synology) and two 3 TB Western Digital Red Drives in RAID 1 config.</p>
<p>I chose this for the x86 processor and the upgradable RAM. The processor meant I could easily run Docker and the RAM was never gonna be a bottleneck again. An SSD cache would have been nice, but was only available in the DS918+ which was significantly more expensive.</p>
<p>In 2019, I added a 5th Gen Intel NUC to my setup, as well. Initially mostly as a Kodi media center, including TVHeadend for dual satellite receivers, this later became my main server.</p>
<p>With the basis of a DS718+, the NUC, and some Raspberry Pis, I changed my setup multiple, some might say many, times over. I will not describe all of these, mostly failed, experiments here in detail, but directly jump to the state my setup has today.
As a summary, I did experiment with RPi clusters, both GlusterFS (generally promising) and Kubernetes as a multi-master, virtual IP setup (not happy with performance and overhead), shifted services around, introduced and retired devices, etc.</p>
<h2 id="2023">2023</h2>
<p>I will try to describe the complete setup as a single tree, following the simplified hardware infrastructure:</p>
<ul>
<li>location one
<ul>
<li><strong>DS210j</strong>, 2x1.5TB RAID 1
<ul>
<li><strong>SMB</strong></li>
<li><strong>rsync</strong> (as offsite backup target)</li>
<li><strong>UPNP Server</strong> (local media distribution)</li>
</ul>
</li>
<li><strong>Raspberry Pi 3B+</strong>
<ul>
<li><strong>Wireshark</strong> (net-to-net gateway)</li>
</ul>
</li>
<li><strong>FritzBox</strong> (as router)</li>
</ul>
</li>
<li>location two
<ul>
<li>3x <strong>D-Link DAP-X1860</strong> (as WiFi APs, with OpenWRT)</li>
<li><strong>FritzBox</strong> (as router)</li>
<li><strong>Netgear GS108E</strong> (as switch)</li>
<li><strong>DS718+</strong>, 2x3TB RAID 1
<ul>
<li>Additional <strong>WD 3TB USB Drive</strong>
<ul>
<li>used as “nice-to-have” storage only, no critical data</li>
</ul>
</li>
<li>Synology packages
<ul>
<li><strong>LDAP</strong> (user management)</li>
<li><strong>Hyper Backup</strong> (versioned backups)</li>
<li><strong>Drive</strong> (as cloud synchronization on all devices)</li>
</ul>
</li>
<li>Dockerized services
<ul>
<li><strong>Portainer</strong> (container management)</li>
<li><strong>Watchtower</strong> (regular updates)</li>
<li><strong>Radicale</strong> (CalDAV/CardDAV server)</li>
<li><strong>Mailserver</strong> (Dovecot, Postfix, Fetchmail; authentication via LDAP)</li>
<li><strong>Rainloop</strong> (Webmail interface)</li>
<li><strong>Glances</strong> (Host performance metrics)</li>
</ul>
</li>
</ul>
</li>
<li><strong>Intel NUC</strong>, i3-5010U, 16 GB RAM, 120 GB M.2 + 256 GB SATA
<ul>
<li><strong>Proxmox</strong>
<ul>
<li><strong>Glances</strong>
<ul>
<li>native</li>
<li>performance metrics</li>
</ul>
</li>
<li><strong>PiHole</strong>
<ul>
<li>Arch Linux VM</li>
<li>DNS server</li>
</ul>
</li>
<li><strong>Wireguard</strong>
<ul>
<li>Arch Linux VM</li>
<li>net-to-net gw</li>
<li>roadwarriors entry point</li>
</ul>
</li>
<li><strong>Docker host</strong> (Arch Linux VM)
<ul>
<li><strong>Portainer</strong> (container management)</li>
<li><strong>Watchtower</strong> (regular updates)</li>
<li><strong>Uptime Kuma</strong> (monitoring solution)</li>
<li><strong>Traefik</strong> (reverse proxy; see also <a href="https://www.mundhenk.org/traefik-authelia-patterns/">Traefik & Authelia Patterns</a>)</li>
<li><strong>Authelia</strong> (authentication & authorization; connected to LDAP)</li>
<li><strong>Heimdall</strong> (central portal, homepage on all laptops)</li>
<li><strong>Ntfy</strong> (notification system)</li>
<li><strong>Miniflux</strong> (RSS reader)</li>
<li><strong>Home Assistant</strong> (home automation solution)</li>
<li><strong>ZigBee2MQTT</strong> (ZigBee connection)</li>
<li><strong>Mosquitto</strong> (MQTT Broker)</li>
<li><strong>Snapdrop</strong> (Airdrop-style file-/textsharing)</li>
<li><strong><a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a></strong> (filetransfer solution)</li>
<li><strong>FreeRADIUS</strong> (RADIUS server; authentication via LDAP)</li>
<li><strong>PhotoPrism</strong> (Photo library with AI enhancements)</li>
<li><strong>Vaultwarden</strong> (Password manager)</li>
<li><strong><a href="https://www.mundhenk.org/analog-document-digitization/">BrotherScannerDocker</a></strong> (Scanner server)</li>
<li><strong><a href="https://www.mundhenk.org/analog-document-digitization/">TesseractOCR</a></strong> (OCR server)</li>
<li><strong><a href="https://docs.linuxserver.io/images/docker-webtop">webtop</a></strong> (web-based, dockerized desktop environment)</li>
<li>Multiple small web services for simple images (e.g., WiFi QR codes), pages (e.g., status displays), etc.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Raspberry Pi 3B+</strong>
<ul>
<li>with <strong><a href="https://www.mundhenk.org/labeler/">labeler</a></strong>, connected to Brother label printer</li>
</ul>
</li>
<li><strong>Raspberry Pi 3B+</strong>
<ul>
<li>connected to TV & sound system</li>
<li><strong>LibreELEC / Kodi</strong> (media center)</li>
</ul>
</li>
<li><strong>Raspberry Pi 1B</strong>
<ul>
<li>connected to two IR transceivers at electricity meter</li>
<li>DIY script to read electricity meter values and publish to MQTT</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="backup-strategy">Backup Strategy</h2>
<p>Having good backups is probably the most important part of any home network.
One can never really go too far here.
In my case, I am running versioned backups of all relevant data from all enduser devices via Synology Drive to the DS718+.
The NUC backs up individual data via rsync nightly to the DS718+.
For Proxmox, I use the built in backup function for all VMs, also nightly to the DS718+.
Note that the Proxmox VMs are the only system backups.
Other than that, I usually only save relevant files, not whole system setups.
From the DS718+, a nightly backup task syncs the shared folders to the DS210j off-site.
Additionally, I am running a nightly Hyper Backup to back up the required folders to the DS210j, where I keep three version.</p>
<p>For all major critical data, such as calendar entries, contacts, e-mails, etc. I make sure to use file backends (e.g., Maildir for Dovecot and multifile for Radicale).
If all servers are down and everything else fails, this allows me to access all data with grep or similar.
Of course this is a performance trade-off that does not work in larger setups.
But with a user base smaller than ten and the hardware I have at my disposal, the performance impact is not a concern.</p>
<h2 id="future-work">Future Work</h2>
<p>Change is a constant in my setup. There is always something to improve. For the near future, among others, I am planning to…</p>
<ul>
<li>replace the GS108E with a GS108T on OpenWRT, mostly for link aggregation and easier management.</li>
<li>separate network a little better with VLANs.</li>
<li>migrate all services (except out-of-band monitoring & management) to Proxmox Docker host.</li>
<li>migrate Synology Directory Server to OpenLDAP.</li>
<li>introduce 802.11x RADIUS authentication for WiFi. Maybe with <a href="https://openwrt.org/docs/guide-user/network/wifi/wireless.security.8021x#x_dynamic_vlans_on_an_openwrt_router">dynamic VLANs for guests</a>.</li>
<li>replace both FritzBoxes with pfsense/opnsense routers.</li>
<li>add influxDB and Grafana to store automatically filtered historical data (e.g., room humidity over certain percentages only, lower resolution power consumption curves, memory/CPU load curves to debug post issues, …).</li>
<li>add a central logging daemon.</li>
<li>introduce a central OpenWRT management solution like RADIUSdesk.</li>
<li>document and automate the system and its setup procedure with infrastructure-as-code.</li>
<li>add a disconnectable USB drive for backups in a weekly rotation system over one month.</li>
</ul>Philipp MundhenkI have been getting multiple requests recently to give an overview over my homelab. I will do this in this article. I will include some of the history, but ignore many of the experiments I tried and failed over the years (Banana Pi, RPi-based GlusterFS, easily scalable Kubernetes on RPis, Docker on ARMv6, etc.).Traefik & Authelia Patterns2023-08-09T00:00:00+00:002023-08-09T00:00:00+00:00https://philippmundhenk.github.io/traefik-authelia-patterns<p>I run a small number of webservices at home behind a <a href="https://traefik.io/traefik/">Traefik</a> & <a href="https://www.authelia.com/">Authelia</a> setup.
Authelia is used for authorization, as well as authentication through a connected LDAP server.
In this setup, I find myself frequently using similar patterns again and again that took some time to figure out, so I document them here.
Note that none of these are my invention, but I did find them hard to come by, so I want to summarize them here.
Maybe they will help someone else, or myself, in future.</p>
<h2 id="basic-auth-middleware">Basic Auth Middleware</h2>
<p>While Authelia offers a great GUI for logins, there are a number of services, that require HTTP Basic Auth.
Example of such services include <a href="https://ntfy.sh/">ntfy.sh</a> and <a href="https://radicale.org/">Radicale</a>.
In Authelia, using basic auth instead of the standard “pretty auth” is fairly easy, as you can just use <code class="language-plaintext highlighter-rouge">auth=basic</code>.
Wrap this in a dedicated middleware and this is very easy to use.</p>
<p>In your Authelia service, add a new middleware, here we call it authelia-basic:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">authelia</span><span class="pi">:</span>
<span class="pi">[</span><span class="nv">...</span><span class="pi">]</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.middlewares.authelia-basic.forwardauth.address=http://authelia:9091/api/verify?auth=basic&rd=https://authelia.example.com'</span> <span class="c1"># yamllint disable-line rule:line-length</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.middlewares.authelia-basic.forwardauth.trustForwardHeader=true'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.middlewares.authelia-basic.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'</span> <span class="c1"># yamllint disable-line rule:line-length</span>
</code></pre></div></div>
<p>You can then use this middleware in your services:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">service</span><span class="pi">:</span>
<span class="pi">[</span><span class="nv">...</span><span class="pi">]</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.enable=true'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.basicAuthService.rule=Host(`basicAuthService.example.com`)'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.basicAuthService.entrypoints=https'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.basicAuthService.tls=true'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.basicAuthService.tls.certresolver=letsencrypt'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.basicAuthService.middlewares=authelia-basic@docker'</span>
</code></pre></div></div>
<h2 id="automatic-middleware-selector">Automatic Middleware Selector</h2>
<p>The downside of the above approach is that one has to decide between basic auth and pretty auth.
Ideally, one would like both, e.g., to access the web interface of ntfy.sh through prett auth, but in case the app is accessing and passing along basic auth information, use basic auth.
<a href="https://github.com/authelia/authelia/issues/2753#issuecomment-1005176988">GitHub user Simske</a> found a great solution to this, by evaluation the request header and chosing the middleware based on the result of this:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">service</span><span class="pi">:</span>
<span class="pi">[</span><span class="nv">...</span><span class="pi">]</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.ntfy.rule=Host(`ntfy.example.com`)'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.ntfy.middlewares=authelia@docker'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.ntfy_basic.rule=Host(`ntfy.example.com`)</span><span class="nv"> </span><span class="s">&&</span><span class="nv"> </span><span class="s">HeadersRegexp(`Authorization`,</span><span class="nv"> </span><span class="s">`Basic</span><span class="nv"> </span><span class="s">.*`)'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">traefik.http.routers.ntfy_basic.middlewares=authelia-basic@docker'</span>
</code></pre></div></div>
<p>Note that with <a href="https://www.authelia.com/blog/4.38-pre-release-notes/">Authelia 4.38</a>, this might no longer be needed when the <a href="https://deploy-preview-5250--authelia-staging.netlify.app/configuration/miscellaneous/server-endpoints-authz/">authz endpoints</a> are introduced.</p>
<h2 id="split-dns">Split DNS</h2>
<p>Often, when accessing services inside the network, one wants to open them up for everyone to use.
However, the same services accessed from outside, should be protected by Authelia.
Authelia easily allows us to set up different rules and bypass for local networks:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">[</span><span class="nv">...</span><span class="pi">]</span>
<span class="pi">-</span> <span class="na">domain</span><span class="pi">:</span> <span class="s">ntfy.example.com</span>
<span class="na">networks</span><span class="pi">:</span>
<span class="err"> </span><span class="pi">-</span> <span class="s">192.168.0.0/24</span>
<span class="err"> </span><span class="s">- 10.0.0.0/24</span>
<span class="err"> </span><span class="s">- 172.16.0.0/16</span>
<span class="s">policy</span><span class="pi">:</span> <span class="s">bypass</span>
<span class="pi">-</span> <span class="na">domain</span><span class="pi">:</span> <span class="s">ntfy.example.com</span>
<span class="na">policy</span><span class="pi">:</span> <span class="s">one_factor</span>
</code></pre></div></div>
<p>This only works though, if the request is received locally.
Thus, a standard request to an endpoint online will be received by Authelia with the external IP address.</p>
<p>Instead, we can use a local DNS server (e.g., the one integrated in PiHole) to deliver different addresses when locally resolving domains.
In the above example, you would map ntfy.example.com to the local IP address of your server, say 192.168.1.100.
This way, the access is performed through the local IP address, rather than the public one and the Authelia bypass rule will kick in, rather than the one_factor login.</p>
<p>Be sure to keep the right order of Authelia rules, of one_factor will always be applied, as no restrictions are specified there.
Also make sure to add all other local networks you might want to access the service, e.g., Docker hosts or VPN clients.</p>
<h2 id="hide-local-ip">Hide local IP</h2>
<p>I like the above Split DNS setup so much that I am using it for almost all of my services.
However, I did stumble across some issues in one service: <a href="https://snapdrop.net/">Snapdrop</a>.
Snapdrop assigns users to rooms based on their IP address.
As such, it is heavily relying on NAT being present.
When accessing the service locally, this is obviously not the case, as one is accessing it from local IP addresses directly.
Instead, I avoid using Split DNS for Snapdrop and always access it through NAT.
This makes all devices on my network appear under the same IP address.
Unfortunately, this also means that users will always have to login, as the above advantages of Split DNS do not apply and I don’t want to make the service publicly available.</p>
<h2 id="man-in-the-middle">Man-in-the-Middle</h2>
<p>Every once-in-a-while two components just don’t want to fit together.
Take Authelia and Radicale for example.
Authelia nicely delivers along the Remote-User HTTP header that a service can trust to have been authenticated by Authelia.
Radicale does offer such HTTP header based authentication, but only with the header X-Remote-User.
Neither of these has an option to calibrate the header name.
To resolve such mismatches, I use a man-in-the-middle style webserver, which present itself to Authelia, translates the header and forwards all requests as reverse proxy to the original service.
Here is an example configuration for Apache:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><VirtualHost *:80>
DocumentRoot "/usr/local/apache2/htdocs/"
ServerName somewhere.example.com
<Location />
ProxyPreserveHost On
ProxyPass http://somewhere-else.example.com connectiontimeout=300 timeout=300
ProxyPassReverse http://somewhere-else.example.com
SetEnvIf Remote-User (.*) saved_remote_user=$1
RequestHeader set X-Remote-User "%{saved_remote_user}e"
</Location>
</VirtualHost>
</code></pre></div></div>
<p>This could also be used to e.g., add static authentication information by hard-coding a username here, though I would obviously not recommend this.
It is also a nice pattern to integrate services running on other hosts into Traefik, as a local instance with according labels can be managed and automatically assigned certificates, etc.
This entity then takes care of getting the request to the correct host.
I use that here and there for a few services that are not running on my main host due to e.g., I/O restrictions.</p>
<p>Note that this is not the highest performing way to solve this issue, but it works fine in a small setup like mine.
If you have performance requirements, you might want to find another solution, as the additional reverse proxy creates compute overhead and can potentially be a bottleneck.
If you are running such a setup, you will likely not be needing to read my blog though.</p>
<h2 id="minimal-bypass">Minimal Bypass</h2>
<p>For the services I do make publicly available, I want to make sure to expose as little attack surface as possible.
I usually start with the simplest possible bypass only, e.g., the domain only.
I then access the website with a browser and open developer tools (e.g., F12 in Firefox) and take a look at the console and network tabs, to see which other resources (js, CSS, etc.) the page is trying to load.
I then, very restrictively allow access to these resources only, until the page loads correctly.
Here, you can either list all resources individually, or, in cases like <a href="https://github.com/psi-4ward/psitransfer">PsiTransfer</a> might also need to include a regular expression for e.g., the generated URLs, e.g.:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">domain</span><span class="pi">:</span> <span class="s">psitransfer.example.com</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="err"> </span><span class="pi">-</span> <span class="s2">"</span><span class="s">^/([a-f]|[0-9]){12}$"</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/([a-f]|[0-9]){12}\.json$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/assets/styles\.css$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/assets/favicon\.ico$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/app/common\.js$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/app/download\.js$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/favicon.ico$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/lang\.json$'</span>
<span class="err"> </span><span class="pi">-</span> <span class="s1">'</span><span class="s">^/files/([a-f]|[0-9]){12}\+\+.*$'</span>
</code></pre></div></div>
<p>Make sure to use <code class="language-plaintext highlighter-rouge">'</code> instead of <code class="language-plaintext highlighter-rouge">"</code> to avoid interpretation of <code class="language-plaintext highlighter-rouge">\.</code> by the yaml parser.</p>
<p>Note that this can be very tedious for complex pages.
But since I am not very comfortable publicly opening complex web pages and increasing my attack surface, I only apply this process for one or two very minimal, selected services.</p>
<h2 id="stable-setup">Stable Setup</h2>
<p>One important item to watch out for is to keep your set of Docker conainers stable.
In one case, I had experimented with a Docker container that kept failing due to a wrong configuration.
It was late one evening and I stopped working on the specific container, but also forgot to turn it off completely.
This resulted in a container continuously restarting every few minutes for days.
Smart? Certainly not, but it happens.
Despite not having anything to do with Traefik, this restart in turn triggered Traefik to re-read the Docker configuration for labels and re-setup all routers, middlewares, etc.
This resulted in breaking client connections and sessions and issues with e.g., web interfaces and APIs in many other unrelated services.</p>Philipp MundhenkI run a small number of webservices at home behind a Traefik & Authelia setup. Authelia is used for authorization, as well as authentication through a connected LDAP server. In this setup, I find myself frequently using similar patterns again and again that took some time to figure out, so I document them here. Note that none of these are my invention, but I did find them hard to come by, so I want to summarize them here. Maybe they will help someone else, or myself, in future.Labeler2022-09-18T00:00:00+00:002022-09-18T00:00:00+00:00https://philippmundhenk.github.io/labeler<p>Recently, I visited Singapore and managed to pick up a <a href="https://www.brother.de/alte-geraete/beschriftungsgeraete/pt-2430pc">Brother P-Touch PT-2430PC</a> for a relatively cheap 40 SGD. I always wanted a label writer, but didn’t want to deal with pressing little buttons and working with a tiny screen. However, having to set it up, working through some Word-like software just for printing a label also didn’t really thrill me. I rather want to be able to quickly print something from my phone. Thus, I assembled a bit of open software and added some on my own to make this happen: <a href="https://github.com/PhilippMundhenk/labeler">Labeler</a>, a web interface for Brother P-Touch.</p>
<h2 id="hardware">Hardware</h2>
<p>The PT-2430PC is ideal, as it has a USB interface, but does not yet electronically check for the authenticity of tapes. Also, it is an old model and available for relatively cheap, while still supporting the current TZe tapes. I attached it to a Raspberry Pi that I have running for other tasks anyway.</p>
<h2 id="driver">Driver</h2>
<p>Unfortunately, Brother does not supply any USB drivers for the PT-2430PC. However, <a href="https://dominic.familie-radermacher.ch/">Dominic Radermacher</a> some years ago created <a href="https://dominic.familie-radermacher.ch/projekte/ptouch-print/">ptouch-print</a>(<a href="https://git.familie-radermacher.ch/linux/ptouch-print.git">git</a>), a driver and interface for the PT-2430PC (and others). This compiles fine also on Raspberry Pi with Arch Linux for ARM.</p>
<h2 id="web-interface">Web Interface</h2>
<p>Ptouch-print is a commandline tool. While I am able to use this, the <a href="https://en.wikipedia.org/wiki/Wife_acceptance_factor">WAF</a> is very low and using it on a smartphone (e.g., via SSH) is also not fun. Thus, a web interface was needed. I quickly whipped up a very basic HTML form with some mini PHP backend, to control at least the most basic features of this printer: Printing single and double lines, as well as showing the installed tape size:</p>
<p><img src="/images/labeler/home.jpg" alt="home" /></p>
<p>A preview of the print (generated by ptouch-print) is also available:</p>
<p><img src="/images/labeler/preview.jpg" alt="preview" /></p>
<p>The design is based on the <a href="https://codepen.io/ainalem/pen/GRqPwoz">“Placeholders” form by Mikael Ainalem</a>.</p>
<h2 id="future-work">Future Work</h2>
<p>Currently, the feature set is very limited. I might extend this in future with things like font selection, image printing, font sizes, etc. depending on my needs. I will only implement what I need, but I am also very open for pull requests on <a href="https://github.com/PhilippMundhenk/labeler">GitHub</a>, if you are missing and have added something.</p>Philipp MundhenkRecently, I visited Singapore and managed to pick up a Brother P-Touch PT-2430PC for a relatively cheap 40 SGD. I always wanted a label writer, but didn’t want to deal with pressing little buttons and working with a tiny screen. However, having to set it up, working through some Word-like software just for printing a label also didn’t really thrill me. I rather want to be able to quickly print something from my phone. Thus, I assembled a bit of open software and added some on my own to make this happen: Labeler, a web interface for Brother P-Touch.A Netbook in 20212021-04-17T00:00:00+00:002021-04-17T00:00:00+00:00https://philippmundhenk.github.io/arch-netbook<p>As everyone knows, I am a huge fan of Linux for many applications. I have a number of Linux-based servers running, I have been using different Linux
dervates for work and privately in virtual machines, but until recently, I did not have a desktop system with Linux. I have been missing that every
once in a while. Some things, such as network administration and software development are just so much easier on Linux. I did not want to spend much
money on an additional machine though, especially since my daily driver will remain on Windows, for family reasons. I thus settled on a netbook. Yes, one of these small screen devices popular in the early 2000s. I figured that it should offer sufficient performance for my use cases, while remaining portable and cheap. Of course, I went with an Arch Linux installation.</p>
<h2 id="hardware">Hardware</h2>
<p>After some research, I decided to go with an Acer Aspire One 522. I managed to get one of these with power supply for less than 80 Euros on a classifieds
platform. I was a bit worried about the battery, so I had the seller send me a photo of the “remaining time” once booted. Of course, this does not say
much, but was sufficient to estimate that runtime should be more than zero.
I was positively surprised when the device arrived as aside from some scratches on the lid, it looked almost new. The battery was not ideal, lasting
about 1.5 hours, but since the power supply is small and I don’t expect it to use it outside of the house, that was sufficient for me. However, after
some calibration (charge fully, discharge fully, charge fully), I managed to reach about 2.5-3 hours. That was ideal for me, as I barely every have
more time than that in a row to “work” on things.</p>
<p>For performance, I did exchange the 1GB RAM with 4GB and replaced the harddrive with an Intel 320, 120GB SSD. Both bought used online for combined about 30 Euros.</p>
<h2 id="arch-linux-installation">Arch Linux Installation</h2>
<p>I do like Arch Linux, especially with its minimal footprint, if desired, and the continuous upgrade proccess. Here are the steps for a minimal Arch
installation. Of course this might vary depending on your setup, so consider this a general guideline.</p>
<h3 id="assumptions">Assumptions</h3>
<p>This write-up assumes</p>
<ul>
<li>BIOS is used, not UEFI</li>
<li>using a SWAP file rather than a SWAP partition</li>
</ul>
<h3 id="preparations">Preparations</h3>
<p>Download Arch from https://archlinux.org/download/ and write to USB stick, e.g., with Win32DiskImager, Rufus, or dd.</p>
<p>Then, on the target machine, boot into the live environment.</p>
<h3 id="installation">Installation</h3>
<ol>
<li>Set keymap for live environment:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> loadkeys de-latin1
</code></pre></div> </div>
</li>
<li>
<p>Connect to internet with either <code class="language-plaintext highlighter-rouge">ip</code> (Ethernet) or <code class="language-plaintext highlighter-rouge">iwctl</code> (WiFi) with <code class="language-plaintext highlighter-rouge">station wlan0 connect <SSID></code>.</p>
</li>
<li>
<p>Update clock: <code class="language-plaintext highlighter-rouge">timedatectl set-ntp true</code></p>
</li>
<li>Partition disks:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> fdisk /dev/sda
<span class="c"># delete all partitions (use 'd')</span>
<span class="c"># create new parition (use 'n' and defaults)</span>
<span class="c"># make bootable (use 'a')</span>
<span class="c"># write to disk (use 'w')</span>
</code></pre></div> </div>
</li>
<li>Format partition:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> mkfs.ext4 /dev/sda1
</code></pre></div> </div>
</li>
<li>Mount partition:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> mount /dev/sda1 /mnt
</code></pre></div> </div>
</li>
<li>Install basic packages:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pacstrap /mnt base-devel linux linux-firmware
</code></pre></div> </div>
</li>
<li>Generate fstab:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> genfstab <span class="nt">-U</span> /mnt <span class="o">>></span> /mnt/etc/fstab
</code></pre></div> </div>
</li>
<li>Move to new system:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> arch-chroot /mnt
</code></pre></div> </div>
</li>
<li>SetHW clock to system time:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hwclock <span class="nt">--systohc</span>
</code></pre></div> </div>
</li>
<li>
<p>Localization:
Uncomment <code class="language-plaintext highlighter-rouge">en_US.UTF-8 UTF-8</code> in <code class="language-plaintext highlighter-rouge">/etc/locale.gen</code> and run <code class="language-plaintext highlighter-rouge">locale-gen</code>
In <code class="language-plaintext highlighter-rouge">/etc/locale.conf</code> put <code class="language-plaintext highlighter-rouge">LANG=en_US.UTF-8</code></p>
</li>
<li>
<p>Configure keyboard layout:
In <code class="language-plaintext highlighter-rouge">/etc/vconsole.conf</code> put <code class="language-plaintext highlighter-rouge">KEYMAP=de-latin1</code></p>
</li>
<li>Configure hostname:
In <code class="language-plaintext highlighter-rouge">/etc/hostname</code> put <code class="language-plaintext highlighter-rouge"><hostname></code>.
In <code class="language-plaintext highlighter-rouge">etc/hosts</code> put:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.0.1 localhost
::1 localhost
127.0.1.1 <<span class="nb">hostname</span><span class="o">></span>.localdomain <<span class="nb">hostname</span><span class="o">></span>
</code></pre></div> </div>
</li>
<li>
<p>Change root password:
Run <code class="language-plaintext highlighter-rouge">passwd</code></p>
</li>
<li>Install networking packages:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> dhcpcd iwctl
</code></pre></div> </div>
</li>
<li>
<p>Install and configure GRUB:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacman <span class="nt">-S</span> grub
<span class="c"># install to drive</span>
grub-install <span class="nt">--target</span><span class="o">=</span>i386-pc /dev/sda
<span class="c"># change GRUB_TIMEOUT in /etc/default/grub</span>
<span class="c"># generate config</span>
grub-mkconfig <span class="nt">-o</span> /boot/grub/grub.cfg
</code></pre></div> </div>
</li>
<li>Reboot to newly installed system</li>
</ol>
<h3 id="post-install">Post Install</h3>
<p>The following optional steps usually make sense:</p>
<ol>
<li>Update system:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pacman <span class="nt">-Syu</span>
</code></pre></div> </div>
</li>
<li>Create user & permissions:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pacman <span class="nt">-S</span> vi <span class="nb">sudo
</span>useradd <span class="nt">-m</span> <user>
visudo
<span class="c"># uncomment 'sudo ALL=(ALL) ALL'</span>
groupadd <span class="nb">sudo
</span>usermod <span class="nt">-a</span> <span class="nt">-G</span> <span class="nb">sudo</span> <user>
passwd <user>
<span class="nb">logout</span>
<span class="c"># log in as <user></span>
</code></pre></div> </div>
</li>
<li>Install yay for AUR:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pacman <span class="nt">-S</span> <span class="nt">--needed</span> git
git clone https://aur.archlinux.org/yay.git
<span class="nb">cd </span>yay
makepkg <span class="nt">-si</span>
</code></pre></div> </div>
</li>
<li>Set timezone:
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> timedatectl set-timezone Europe/Berlin
</code></pre></div> </div>
</li>
<li>
<p>Install GUI:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> yay <span class="nt">-S</span> i3-wm i3lock i3status dmenu xorg-server ttf-dejavu
<span class="c"># in ~/.xinitrc put: 'exec i3'</span>
<span class="c"># in ~/.bash_profile put:</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="nv">$DISPLAY</span> <span class="o">]]</span> <span class="o">&&</span> <span class="o">[[</span> <span class="si">$(</span><span class="nb">tty</span><span class="si">)</span> <span class="o">=</span> /dev/tty1 <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span>startx
<span class="k">fi</span>
</code></pre></div> </div>
</li>
<li>
<p>Configure i3:
In <code class="language-plaintext highlighter-rouge">/~/.config/i3/config</code> add:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> mode "exit: [e]xit, [r]eboot, [s]hutdown, loc[k]" {
bindsym e exec i3-msg exit
bindsym r exec systemctl reboot
bindsym s exec systemctl shutdown
bindsym k exec i3lock; mode "default"
bindsym Escape mode "default"
bindsym Return mode "default"
}
bindsym $mod+x mode "exit: [e]xit, [r]eboot, [s]hutdown, loc[k]"
exec "setxkbmap -layout de"
</code></pre></div> </div>
</li>
</ol>
<h2 id="performance--evaluation">Performance & Evaluation</h2>
<p>I was surprised about the performance of the device, booting to login screen in about 20 seconds, plus an additional about 3 seconds to login and load
i3. This is absolutely adequate for a 10+ year old device.</p>
<p>Typing this long text, the only issue I have with the device is the size of keyboard and screen: They are totally usable, but it is just no ThinkPag
keyboard and the screen is a bit small for long texts. But that is of course expected of a netbook and I could always use my larger ThinkPad T430 for
this.</p>
<p>Web surfing is of course also not the fastest experience ever seen, but perfectly fine for my needs on this device.</p>
<p>So overall, I am extremely happy with the money invested. For a total of about 150 Euro, I have a very portable, decently performing native Linux
device.</p>
<h2 id="references">References</h2>
<p><a href="https://wiki.archlinux.org/index.php/installation_guide">Arch Wiki - Installation Guide</a></p>Philipp MundhenkAs everyone knows, I am a huge fan of Linux for many applications. I have a number of Linux-based servers running, I have been using different Linux dervates for work and privately in virtual machines, but until recently, I did not have a desktop system with Linux. I have been missing that every once in a while. Some things, such as network administration and software development are just so much easier on Linux. I did not want to spend much money on an additional machine though, especially since my daily driver will remain on Windows, for family reasons. I thus settled on a netbook. Yes, one of these small screen devices popular in the early 2000s. I figured that it should offer sufficient performance for my use cases, while remaining portable and cheap. Of course, I went with an Arch Linux installation.kapps2020-12-09T00:00:00+00:002020-12-09T00:00:00+00:00https://philippmundhenk.github.io/kapps<p>I am a big fan of the technology in the Kindle. I am not talking about ebooks and Amazon’s setup to sell them in proprietary formats, DRM, etc. But the E Ink display of the Kindle, the thinness of the device with high energy efficiency and long battery runtimes. Some time ago I thus used a Kindle Touch to build a <a href="/kindle-alarm-clock/">Kindle Alarm Clock</a>. There, I am using a HTML frontend, shown in the webbrowser of the Kindle with jailbreak and connecting to a backend webserver, implemented in Python and running on the Kindle, as well. When building it, it dawned on me that one could extract this setup into an app framework to make it easier for developers to develop apps for Kindles. This would allow great reusability of older devices, which are rather cheap and might otherwise end up in trash. A complete waste of an excellent technology. Thus, I set out to create <a href="https://github.com/PhilippMundhenk/kapps">kapps, a framework for apps on Kindle</a>. The following article is a copy of the <a href="https://github.com/PhilippMundhenk/kapps">kapps README</a> at the time of writing. There is also a <a href="https://github.com/PhilippMundhenk/kapps/wiki">wiki</a> available with more information on its usage.</p>
<h2 id="motivation">Motivation</h2>
<p>Some time ago, I built and <a href="/kindle-alarm-clock/">Alarm Clock for a Kindle Touch</a>. This was based on a HTML GUI, and a Python backend. It was kind of hacked together, but did the job of re-purposing an old Kindle Touch and using its very energy-efficient setup with E Ink display for a custom user interface. While one can generally built upon that to create own applications, the overhead to start a project is relatively high, multiple applications on a single device are potentially incompatible, and launching them might be difficult. I thus for a long time had the plan to extend the setup used for the alarm clock into a framework, which developers can use to create their own applications more easily. Since I have been working on different automotive software platforms, it seemed easy enough to do this in a context with less requirements on reliability and dependability.</p>
<p>The framework is rather rough around the edges for now and gives lots of freedom to the developer, both in terms of backend implementation, as well as frontend. Of course it might be helpful to follow some guidelines, which I shall write in future, but I want to leave it as open as possible, to allow the developer to adjust to his application.</p>
<p>The options for what can be implemented are endless. Here are some ideas:</p>
<ul>
<li>Alarm Clock (I shall port the existing one to kapps in future)</li>
<li>Home Automation control / status display (e.g., calendar, commute times, (light) switches, …)</li>
<li>TV remote control with only the required favorite buttons</li>
<li>shortcuts for PC control (like an Elgato Stream Deck)</li>
<li>Audio (Music, radio, …) player (Kindle Touch has audio out port)</li>
<li>No frills recipe book for kitchen</li>
<li>Learning apps (e.g., vocabulary trainer, math trainer, …)</li>
<li>…</li>
</ul>
<h2 id="features">Features</h2>
<p>kapps is still very basic, it currently contains mainly these features, but may be extended as required:</p>
<ul>
<li>Setup for GUI to backend connection, no need to handle webserver, URLs, etc.</li>
<li>Application definitions, including (un-)installing from web</li>
<li>Commands for communication between applications and GUI</li>
<li>Notification system</li>
</ul>
<h2 id="concepts">Concepts</h2>
<p>The complete system is based on three components: Frontend, backend and commands.</p>
<p>The frontend is typically HTML pages shown in a web browser. The backend is a Python app, loaded by and integrated with the framework. Such backend app is typically called by a command, which may be coming from the frontend, and may return an HTTPResponse to be shown in the frontend. Commands are generally used to communicate between backend apps, as well as the GUI and apps.</p>
<p>As you can see, commands are essential for connecting the components. Some commands are built-in (e.g., Notify, Quit, Launcher), but every app can bring their own commands. One might compare them with Intents in Android. Commands can also be translated to URLs. This allows embeddeding them into the returned HTML shown on the frontend. The integrated webserver will catch such command calls and call the subscribed Python method with the given parameters.</p>
<p>Some more technical notes:</p>
<ul>
<li>Commands are always synchronous</li>
<li>The complete system is operating in one thread. The developer is free to create their own threads.</li>
<li>There are no security features built in, no separation between apps exist. Every app has full access to the system, other apps, as well as your Kindle!</li>
<li>The system is built as flexible as possible. Almost everything is implemented as apps, this includes even the launcher, notifications, installer, etc. Additionally, as common in Python, no type safety is enforced (e.g., for commands)</li>
<li>Publishers and subscribers of commands have an n:m relation: Multiple publishers might issue commands, multiple subscribers may receive them. Each for the receivers may return data. Thus, a call to publish() may return a list of answers.</li>
<li>For the frontend, no frame is enforced. Thus, the developer of an app needs to ensure that the user can return to the kapps screen or go to other apps, e.g., by providing an exit button. This has been chosen to give absolute flexibility to the frontend developer.</li>
</ul>
<h2 id="installation">Installation</h2>
<p>You first need to fulfill a number of requirements:</p>
<h3 id="requirements">Requirements</h3>
<ul>
<li>Kindle Touch: Likely also running on other Kindles (or maybe other devices, with adaptations), but not tested.</li>
<li>Jailbreak for Kindle Touch, see <a href="https://www.mobileread.com/forums/showthread.php?t=275877">here</a></li>
<li>(optional) USB Networking, see <a href="https://www.mobileread.com/forums/showthread.php?t=186645">here</a></li>
<li>WebLaunch, see <a href="https://github.com/PaulFreund/WebLaunch">here</a></li>
<li>Python, see <a href="https://www.mobileread.com/forums/showthread.php?t=225030">here</a></li>
</ul>
<p>Technically, WebLaunch is not required, as it uses the internal browser, but it is a nice wrapper, that I have not yet fully integrated. It is also an extension for KUAL, the Kindle Unified Application Launcher, which we will not be using.</p>
<h3 id="kapps">kapps</h3>
<p>After all requirements are fulfilled, simply copy the folders etc/ and mnt/ from this repo to your Kindle’s file system root (/).</p>
<h2 id="application-examples">Application Examples</h2>
<p>To show how easy a simple application can be, here are two examples, with and without GUI.</p>
<h3 id="basic-application">Basic Application</h3>
<p>The Quit application is a very basic application without GUI. It consists of only a few files:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Quit/
├─ res/
│ ├─ icon.png
├─ __init__.py
├─ quit.py
</code></pre></div></div>
<p>While the <strong>init</strong>.py only marks the existence of a module and is empty, the implementation contained in the quit.py is as simple as this:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">core.kapp</span> <span class="kn">import</span> <span class="n">Kapp</span>
<span class="kn">from</span> <span class="nn">core.commands</span> <span class="kn">import</span> <span class="n">Quit</span><span class="p">,</span> <span class="n">Launcher</span>
<span class="kn">from</span> <span class="nn">core.httpResponse</span> <span class="kn">import</span> <span class="n">HTTPResponse</span>
<span class="k">class</span> <span class="nc">QuitApp</span><span class="p">(</span><span class="n">Kapp</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="s">"Quit"</span>
<span class="k">def</span> <span class="nf">homeCallback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">kcommand</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">Quit</span><span class="p">())</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">Launcher</span><span class="p">())[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">iconCallback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">kcommand</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HTTPResponse</span><span class="p">(</span><span class="n">content</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">getRes</span><span class="p">(</span><span class="s">"icon.png"</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="n">appID</span><span class="p">,</span> <span class="n">appPath</span><span class="p">,</span> <span class="n">ctx</span><span class="p">):</span>
<span class="k">return</span> <span class="n">QuitApp</span><span class="p">(</span><span class="n">appID</span><span class="p">,</span> <span class="n">appPath</span><span class="p">,</span> <span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>By default, every app implements at least the register() method. This creates a Kapp object, implementing the functionality. By default, a Kapp object subscribes to the Home and Icon commands, linking them to the homeCallback() and iconCallback() respectively.
When listing the app in the launcher (potentially other places), the Icon command is issued for that app and consequently, the iconCallback() is called to retrieve the icon. This does not need to be static (see e.g., Notifications app).
When starting the app, the Home command is issued and consequently the homeCallback() is called, potentially returning an HTTPResponse (see below), or itself calling another command, e.g., to start the launcher.</p>
<h3 id="basic-gui-application">Basic GUI Application</h3>
<p>A simple GUI application can be as simple as this:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">core.kapp</span> <span class="kn">import</span> <span class="n">Kapp</span>
<span class="kn">from</span> <span class="nn">core.commands</span> <span class="kn">import</span> <span class="n">Notify</span>
<span class="kn">from</span> <span class="nn">core.httpResponse</span> <span class="kn">import</span> <span class="n">HTTPResponse</span>
<span class="k">class</span> <span class="nc">TestApp</span><span class="p">(</span><span class="n">Kapp</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="s">"TestApp"</span>
<span class="k">def</span> <span class="nf">homeCallback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">kcommand</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">Notify</span><span class="p">().</span><span class="n">setParam</span><span class="p">(</span><span class="s">"title"</span><span class="p">,</span> <span class="s">"Test"</span><span class="p">).</span><span class="n">setParam</span><span class="p">(</span>
<span class="s">"message"</span><span class="p">,</span> <span class="s">"TestApp started"</span><span class="p">))</span>
<span class="k">return</span> <span class="n">HTTPResponse</span><span class="p">(</span><span class="n">content</span><span class="o">=</span><span class="s">"<html><h1>TestApp Home Screen</h1></html>"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">iconCallback</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">kcommand</span><span class="p">):</span>
<span class="k">return</span> <span class="n">HTTPResponse</span><span class="p">(</span><span class="n">content</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">getRes</span><span class="p">(</span><span class="s">"icon.png"</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="n">appID</span><span class="p">,</span> <span class="n">appPath</span><span class="p">,</span> <span class="n">ctx</span><span class="p">):</span>
<span class="k">return</span> <span class="n">TestApp</span><span class="p">(</span><span class="n">appID</span><span class="p">,</span> <span class="n">appPath</span><span class="p">,</span> <span class="n">ctx</span><span class="p">)</span>
</code></pre></div></div>
<p>The skeleton of this app is identical to the non-GUI app. However, we see that the homeCallback() here returns an HTTPResponse, resulting in a website being shown. Note that the given HTTPResponse is not optimal, as it traps the user on the page with no way to return to kapps.
Also note, how the Notify command is being used here to send a notification to the system, when the app is started.</p>
<h2 id="screenshots">Screenshots</h2>
<p>These screenshots are taken directly from the Kindle in kapps using the Screenshot command.</p>
<h3 id="launcher">Launcher</h3>
<p><img src="/images/kapps/launcher.png" alt="Launcher" /></p>
<h3 id="installer">Installer</h3>
<p><img src="/images/kapps/installer.png" alt="Installer" /></p>
<h3 id="uninstaller">Uninstaller</h3>
<p><img src="/images/kapps/uninstaller.png" alt="Uninstaller" /></p>
<h3 id="notifications">Notifications</h3>
<p><img src="/images/kapps/notifications.png" alt="Notifications" /></p>
<h3 id="gallery">Gallery</h3>
<p><img src="/images/kapps/gallery.png" alt="Gallery" />
<img src="/images/kapps/gallery2.png" alt="Gallery" /></p>
<h2 id="improvements">Improvements</h2>
<p>I only built what I needed so far, the feature set might change in future, upon needs and requests. Here are some things on the very top of the list, but in no particular order:</p>
<ul>
<li>A proper, extensible logging system</li>
<li>Tests</li>
<li>An improved documentation, including API and examples</li>
<li>Minimizing dependencies: Use browser directly without WebLaunch</li>
<li>Easier install procedure</li>
<li>Wrap more lipc commands (see <a href="https://wiki.mobileread.com/wiki/Lipc">MobileRead Wiki</a>)</li>
<li>Self-update of core and minimize applications in core</li>
<li>Dependency management for applications</li>
<li>Better update management for applications (e.g., by including and checking version numbers)</li>
<li>Support for more repositories to install</li>
<li>Storage application for non-volatile storage (e.g., key-value store)</li>
<li>Settings file and application</li>
<li>WebSockets or similar to make GUI more responsive</li>
<li>Extend support to other Kindles or other devices</li>
<li>Extend support for non-touch devices</li>
<li>Terminal for interacting with the system (e.g., manually sending/inspecting commands, listing loaded applications, general debugging, etc.)</li>
<li>Support for automatically starting an app on boot</li>
<li>Support for landscape apps</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>kapps is an easy way to get started with app development for Kindles. While it is still very basic, it contains all the required elements to develop apps, including GUI apps which can be designed freely. The setup procedure with jailbreaking a Kindle might be an initial hurdle, but following the links above, it is rather easy. There is a lot of potential for kapps to be extended. I might do that as I have time.</p>Philipp MundhenkI am a big fan of the technology in the Kindle. I am not talking about ebooks and Amazon’s setup to sell them in proprietary formats, DRM, etc. But the E Ink display of the Kindle, the thinness of the device with high energy efficiency and long battery runtimes. Some time ago I thus used a Kindle Touch to build a Kindle Alarm Clock. There, I am using a HTML frontend, shown in the webbrowser of the Kindle with jailbreak and connecting to a backend webserver, implemented in Python and running on the Kindle, as well. When building it, it dawned on me that one could extract this setup into an app framework to make it easier for developers to develop apps for Kindles. This would allow great reusability of older devices, which are rather cheap and might otherwise end up in trash. A complete waste of an excellent technology. Thus, I set out to create kapps, a framework for apps on Kindle. The following article is a copy of the kapps README at the time of writing. There is also a wiki available with more information on its usage.Detecting Laptop Dock2020-12-01T00:00:00+00:002020-12-01T00:00:00+00:00https://philippmundhenk.github.io/detecting-laptop-dock<p>In my <a href="/screen-backlight/">last post</a>, I showed how I automate the LEDs mounted behind my computer screen to turn on and off automatically, when I log in or log out of the computer, respectively. There is a small caveat though: My computer is a laptop. Thus, it is possible that it is not in its docking station at the <a href="/desk-setup/">desk</a>, but e.g., with me on the couch. In this case, I of course don’t want to turn on the LEDs. Thus, I need to detect the docking state of the laptop and notify Home Assistant of it. I then can trigger the lights based on the combined input of the log in/out and un-/docked state. This article will focus on a ThinkPad running Windows.</p>
<h2 id="method">Method</h2>
<p>When looking to detect my ThinkPad dock, I found a number of solutions for Linux, where this is an easy task, using udev. On Windows however, this is not quite as easy. Note that I am not too familiar with Windows, so I try my best to explain all the items and their interconnections here, but I might be a little off sometimes.</p>
<p>After a long search, I came across <a href="https://sandyzeng.com/use-powershell-detect-if-lenovo-laptop-is-attached-docks/">this article</a> by Sandy Zeng. It very nicely shows how to figure out if the USB hub in the dock is connected to the laptop. While the exact script did not work for me, it pointed me in the right direction: Detecting the USB hub. It also gave some device IDs, making it much easier to find my own dock. No surprise, the vendor ID (VID) is the same on my dock as well: <strong>USB\VID_17EF</strong>.</p>
<p>Note: If you are using a different manufacturer and dock, you can find your dock by running “Get-WmiObject Win32_USBControllerDevice” in PowerShell once when docked and once when undocked and comparing the output. Make sure to have no other devices connected to your dock when doing this. You can search for the vendor IDs of the differing items on Google (if more than one item differs) and find the one corresponding to the manufacturer of your dock.</p>
<p>From here on, we have two possibilities:</p>
<ul>
<li>We can poll the existence of this device with a script closely resembling Sandy’s, or</li>
<li>we ask Windows to notify us whenever a device with this ID dis-/appears vis WMI Events.</li>
</ul>
<p>Lets first have a look at the latter, as polling seems somewhat inefficient. Spoiler alert: Later, we will see reality is not quite as simple.</p>
<h3 id="wmi-events">WMI Events</h3>
<p><a href="https://en.wikipedia.org/wiki/Windows_Management_Instrumentation">Windows Management Instructions (WMI)</a> allows to get a large amount of information from your system. This includes the existing connection of a device. It also allows to register a callback to certain events, such as an <a href="https://docs.microsoft.com/en-us/windows/win32/wmisdk/--instancecreationevent">“InstanceCreationEvent”</a>, when a device is attached and its driver is loaded, or the opposite <a href="https://docs.microsoft.com/en-us/windows/win32/wmisdk/--instancedeletionevent">“InstanceDeletionEvent”</a> when the device is removed. The WMI class we are interested in, we learn from Sandy’s article is a <a href="https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-usbcontrollerdevice">“Win32_USBControllerDevice”</a>.</p>
<p>We can now register a script to be called when the above events happen for the specified class. This called script can then check if the device added/removed is the dock, based on the vendor ID. To register the callbacks for adding/removing devices, use use something like this:</p>
<div class="language-posh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SELECT * FROM __InstanceCreationEvent WITHIN 30 WHERE TargetInstance ISA 'Win32_USBControllerDevice'"</span><span class="p">;</span><span class="w">
</span><span class="nv">$Action</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="o">&</span><span class="w"> </span><span class="n">C:\detectDocked.ps1</span><span class="p">;</span><span class="w"> </span><span class="p">};</span><span class="w">
</span><span class="n">Register-WmiEvent</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="nv">$Query</span><span class="w"> </span><span class="nt">-Action</span><span class="w"> </span><span class="nv">$Action</span><span class="w"> </span><span class="nt">-SourceIdentifier</span><span class="w"> </span><span class="nx">LaptopDocked</span><span class="p">;</span><span class="w">
</span><span class="nv">$Query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"SELECT * FROM __InstanceDeletionEvent WITHIN 30 WHERE TargetInstance ISA 'Win32_USBControllerDevice'"</span><span class="p">;</span><span class="w">
</span><span class="nv">$Action</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="o">&</span><span class="w"> </span><span class="n">C:\detectUndocked.ps1</span><span class="p">;</span><span class="w"> </span><span class="p">};</span><span class="w">
</span><span class="n">Register-WmiEvent</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="nv">$Query</span><span class="w"> </span><span class="nt">-Action</span><span class="w"> </span><span class="nv">$Action</span><span class="w"> </span><span class="nt">-SourceIdentifier</span><span class="w"> </span><span class="nx">LaptopUndocked</span><span class="p">;</span><span class="w">
</span></code></pre></div></div>
<p>This script will register callbacks to the InstanceCreationEvent and the InstanceDeletionEvent, calling the scripts C:\detectDocked.ps1 and C:\detectUndocked.ps1.</p>
<p>The query is written in WQL, something Microsoft calls <a href="https://docs.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi">SQL for WMI</a>, as it loosely resembles SQL. While most of this query is rather straight-forward, take note of the <a href="https://docs.microsoft.com/en-us/windows/win32/wmisdk/within-clause">WITHIN clause</a>. This determines how often this query is run against the WMI database. In other words: It is a form of polling. Reading on a little, <a href="https://devblogs.microsoft.com/scripting/use-powershell-to-create-a-permanent-wmi-event-to-launch-a-vbscript/">we find</a> that it is recommended to keep the WITHIN clause above 30, as otherwise workload on the machine might be too high. This leads to detection delays of up to 30 seconds when un-/docking. I find such delays not acceptable. If a system is overloaded by asking more often than twice a minute if a driver is loaded, this does not sound very efficient (putting aside the obvious fact that I need to poll in the first place).</p>
<p>Thus, we might as well save ourselves all this trouble of registering callbacks and simply poll ourselves, but in a faster manner:</p>
<h3 id="polling">Polling</h3>
<p>While pretty much every computer scientist will tell you polling is not a good idea in almost all cases, I can today refer to not being a computer scientist and use polling anyway. As we have seen above, there is unfortunately no other option. Thus, we extend on Sandy’s script to detect both an existing and a missing dock, add some logic to not run unnecessary script calls and put all of that in an infinite loop, with a timeout of 5 seconds, which is much more acceptable than the 30 seconds used for WMI queries. Save the following e.g., in C:\dock\dockDetector.ps1</p>
<div class="language-posh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">bool</span><span class="p">]</span><span class="w"> </span><span class="nv">$DockedInLastRun</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="kr">while</span><span class="p">(</span><span class="mi">1</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$Manufacturer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-WmiObject</span><span class="w"> </span><span class="nx">win32_computersystem</span><span class="p">)</span><span class="o">.</span><span class="nf">Manufacturer</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Manufacturer</span><span class="w"> </span><span class="o">-like</span><span class="w"> </span><span class="s2">"Lenovo"</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="kr">try</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$DeviceNames</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-WmiObject</span><span class="w"> </span><span class="nx">Win32_USBControllerDevice</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">[</span><span class="n">wmi</span><span class="p">](</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Dependent</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">DeviceID</span><span class="w"> </span><span class="o">-like</span><span class="w"> </span><span class="s2">"*usb\vid_17ef&pid*"</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w"> </span><span class="nx">Manufacturer</span><span class="p">,</span><span class="w"> </span><span class="nx">Description</span><span class="p">,</span><span class="w"> </span><span class="nx">DeviceID</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="nt">-GroupBy</span><span class="w"> </span><span class="nx">Manufacturer</span><span class="w"> </span><span class="nx">Description</span><span class="p">,</span><span class="w"> </span><span class="nx">Service</span><span class="p">,</span><span class="w"> </span><span class="nx">DeviceID</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="w">
</span><span class="nv">$dir</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Split-Path</span><span class="w"> </span><span class="nt">-Parent</span><span class="w"> </span><span class="bp">$MyInvocation</span><span class="o">.</span><span class="nf">MyCommand</span><span class="o">.</span><span class="nf">Path</span><span class="w">
</span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DeviceNames</span><span class="w"> </span><span class="o">-And</span><span class="w"> </span><span class="o">!</span><span class="nv">$DockedInLastRun</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="n">cmd.exe</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="s2">"</span><span class="nv">$dir</span><span class="s2">\docked.bat"</span><span class="w"> </span><span class="err">></span><span class="bp">$null</span><span class="w"> </span><span class="nx">2</span><span class="err">></span><span class="o">&</span><span class="nx">1</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$LASTEXITCODE</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$DockedInLastRun</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$DeviceNames</span><span class="w"> </span><span class="o">-And</span><span class="w"> </span><span class="nv">$DockedInLastRun</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="n">cmd.exe</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="s2">"</span><span class="nv">$dir</span><span class="s2">\undocked.bat"</span><span class="w"> </span><span class="err">></span><span class="bp">$null</span><span class="w"> </span><span class="nx">2</span><span class="err">></span><span class="o">&</span><span class="nx">1</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$LASTEXITCODE</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$DockedInLastRun</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="c"># write-host $Error[0]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-s</span><span class="w"> </span><span class="nx">5</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This script will run continuously and call the Batch script docked.bat, if the dock has appeared in the last 5 seconds, or undocked.bat if the dock has disappeared in the last 5 seconds. The scripts need to be located in the same folder as this script.</p>
<p>For reference, I am calling Home Assistant to set input_boolean.laptop_docked to true, if the dock appeared (C:\dock\docked.bat):</p>
<div class="language-bat highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">curl</span> <span class="na">-X </span><span class="kd">POST</span> <span class="na">-H </span><span class="s2">"Authorization: Bearer <TOKEN>"</span> <span class="na">-H </span><span class="s2">"Content-Type: application/json"</span> <span class="na">-d </span><span class="s2">"{\"</span><span class="kd">entity_id</span>\<span class="s2">": \"</span><span class="kd">input_boolean</span>.laptop_docked\<span class="s2">"}"</span> <span class="kd">http</span>://<IP>:<PORT>/api/services/input_boolean/turn_on
<span class="kd">EXIT</span> $<span class="o">?</span>
</code></pre></div></div>
<p>and set it to false, if the dock disappeared (C:\dock\undocked.bat):</p>
<div class="language-bat highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">curl</span> <span class="na">-X </span><span class="kd">POST</span> <span class="na">-H </span><span class="s2">"Authorization: Bearer <TOKEN>"</span> <span class="na">-H </span><span class="s2">"Content-Type: application/json"</span> <span class="na">-d </span><span class="s2">"{\"</span><span class="kd">entity_id</span>\<span class="s2">": \"</span><span class="kd">input_boolean</span>.laptop_docked\<span class="s2">"}"</span> <span class="kd">http</span>://<IP>:<PORT>/api/services/input_boolean/turn_off
<span class="kd">EXIT</span> $<span class="o">?</span>
</code></pre></div></div>
<p>For details on these commands, see my <a href="/screen-backlight/">last post</a>.</p>
<h2 id="triggering">Triggering</h2>
<p>To make sure that this script is always running, we want to put it in the common start folder for all users. To do so, pres Win+R and type: “shell:common startup”.</p>
<p>In the opening folder, create a new shortcut with thew following properties:</p>
<ul>
<li>Target: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden -Command “C:\dock\dockDetector.ps1”</li>
<li>Start in: C:\dock\</li>
<li>Run: minimized</li>
</ul>
<p>This will make sure that the script is started when a user logs in and also minimizes and hides it, making sure the user is not obstructed or confused by it.</p>
<h2 id="bonus-home-assistant-config">Bonus: Home Assistant Config</h2>
<p>As initially mentioned, I want to use the docking state, in combination with the login state to switch on/off my LED lights. I thus change the automations shown in my <a href="/screen-backlight/">last post</a> to this:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">turn_on_screen_backlight</span>
<span class="na">alias</span><span class="pi">:</span> <span class="s">Turn on screen backlight</span>
<span class="na">trigger</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop</span>
<span class="na">platform</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">to</span><span class="pi">:</span> <span class="s1">'</span><span class="s">on'</span>
<span class="pi">-</span> <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop_docked</span>
<span class="na">platform</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">to</span><span class="pi">:</span> <span class="s1">'</span><span class="s">on'</span>
<span class="na">condition</span><span class="pi">:</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">and</span>
<span class="na">conditions</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop</span>
<span class="na">state</span><span class="pi">:</span> <span class="s1">'</span><span class="s">on'</span>
<span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop_docked</span>
<span class="na">state</span><span class="pi">:</span> <span class="s1">'</span><span class="s">on'</span>
<span class="na">action</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">service</span><span class="pi">:</span> <span class="s">switch.turn_on</span>
<span class="na">data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.plug4</span>
<span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">turn_off_screen_backlight</span>
<span class="na">alias</span><span class="pi">:</span> <span class="s">Turn off screen backlight</span>
<span class="na">trigger</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop</span>
<span class="na">platform</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">to</span><span class="pi">:</span> <span class="s1">'</span><span class="s">off'</span>
<span class="pi">-</span> <span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop_docked</span>
<span class="na">platform</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">to</span><span class="pi">:</span> <span class="s1">'</span><span class="s">off'</span>
<span class="na">condition</span><span class="pi">:</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">or</span>
<span class="na">conditions</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop</span>
<span class="na">state</span><span class="pi">:</span> <span class="s1">'</span><span class="s">off'</span>
<span class="pi">-</span> <span class="na">condition</span><span class="pi">:</span> <span class="s">state</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">input_boolean.laptop_docked</span>
<span class="na">state</span><span class="pi">:</span> <span class="s1">'</span><span class="s">off'</span>
<span class="na">action</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">service</span><span class="pi">:</span> <span class="s">switch.turn_off</span>
<span class="na">data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.plug4</span>
</code></pre></div></div>
<p>These automations will trigger whenever one of the input_booleans changes to on/off and make sure the light is turned on when both input_booleans are on (laptop is docked, a user is logged in) and off in all other cases, effectively building a logical AND.</p>
<h2 id="conclusion">Conclusion</h2>
<p>While it is not always easy, rather inefficient because of polling, and costs me and inordinate amount of time to set up, it is possible to get the docking state of the laptop in Windows to pass to Home Assistant. This will come in handy for a number of automations all around my desk.</p>Philipp MundhenkIn my last post, I showed how I automate the LEDs mounted behind my computer screen to turn on and off automatically, when I log in or log out of the computer, respectively. There is a small caveat though: My computer is a laptop. Thus, it is possible that it is not in its docking station at the desk, but e.g., with me on the couch. In this case, I of course don’t want to turn on the LEDs. Thus, I need to detect the docking state of the laptop and notify Home Assistant of it. I then can trigger the lights based on the combined input of the log in/out and un-/docked state. This article will focus on a ThinkPad running Windows.