The Millhouse Group Blog
Software Development in the 21st Century
Tuesday, 1 July 2025
Mikrotik - automation
After a [multi-step migration process](https://blog.themillhousegroup.com/search/label/mikrotik), we've finally got the Mikrotik RB2011 router doing routery-things on our local network, freeing up the Raspberry Pi that was previously somewhat shackled to this task. Now we can actually get to the main reason for adding the RB2011 in the first place - __automation__.
Why would you want to do this?
### Certbot automation
Every now and again, your Let's Encrypt certificate needs to be renewed. With [shorter certificate lifespans](https://www.digicert.com/blog/tls-certificate-lifetimes-will-officially-reduce-to-47-days) on the horizon, [automation of certificate renewal becomes absolutely non-optional](https://www.digicert.com/blog/why-certificate-automation-is-an-absolute-must).
Excellently, the Let's Encrypt `certbot` tool [supports `--pre-hook` and `--post-hook` options](https://eff-certbot.readthedocs.io/en/stable/using.html#certbot-command-line-options), where you can perform tasks to make the `certbot` process work properly in your particular setting - for example, shutting down a server on port 80 that would prevent `certbot` from being able to listen on that port during the renewal process, and then restarting it again once `certbot` is done.
### Extra security
WireGuard is great, but leaving the WireGuard port open all the time, to the whole internet, seems a little too inviting. By automating a firewall rule we can allow IP address *"a.b.c.d"* ONLY when WE want access, and default to keeping that port inaccessible.
### an SO-Friendly Kid Control
As mentioned in the [previous post](https://blog.themillhousegroup.com/2025/06/mikrotik-routerboard-kid-control.html), the RouterOS Kid Control feature is excellent, but it's not exactly Significant-Other-friendly. Stand by for a future post where I explore this aspect more thoroughly...
#### Let's Do It. Let's Encrypt
For our first bit of automation, we're going to sort out the __Let's Encrypt__ `certbot` so that it can safely be run from `cron`, by routing port 80 to the `certbot` machine temporarily (in the `--pre-hook`) and then reverting that route after the certificate was renewed (in the `--post-hook`).
Of course web traffic forwarding is always a little tricky when the device doing the forwarding *also* has a web interface on port 80, so I'll *double*-port-forward this via port 8888!
Here are the network *pre*-conditions for this:
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
service ispr(affcircle:c-firewall-green) [ISP Router] in home
group mik(affcircle:c-router)[RB2011] in home
service gbe(affcircle:c-switch-red)[Blocks] in mik
service pi(affcircle:c-server-blue)[Pi] in home
internet:B -[Port 80]-> T:ispr
ispr:B -[Port 8888]-> T:gbe
gbe:B -- T:pi
```
Our ISP router just blindly forwards port 80 as port 8888 to the Mikrotik, which duly drops everything. The Pi is *physically* connected to the RB2011 but never gets those packets.
*After* we've run our script, we want:
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
service ispr(affcircle:c-firewall-green) [ISP Router] in home
group mik(affcircle:c-router)[RB2011] in home
service gbe(affcircle:c-switch-green)[Allows] in mik
service pi(affcircle:c-server-green)[Pi] in home
internet:B <-[Port 80]-> T:ispr
ispr:B <-[Port 8888]-> T:gbe
gbe:B <-[Port 80]-> T:pi
```
Port 8888 traffic is forwarded across the router, switched *back* to port 80, and sent to the Pi, with a suitable return path configured via NAT.
All we really want here is to have (similarly to our [Kid-Control solution](https://blog.themillhousegroup.com/2025/06/mikrotik-routerboard-kid-control.html)) a router-traversing NAT path, but rather than the main rule being Source-NAT (because we're NATting network packets heading for the outside-world), this time the main rule is a `dstnat` that allows packets from the outside world to find their way to the Pi. For RouterOS, this translates to the following additions to the firewall:
- A `forward` filter rule so that incoming packets don't hit the floor
- A `dstnat` NAT rule (aka port-forwarding) so that packets for the router go to the Pi; AND
- Because the Mikrotik is NOT the ISP router (and so the Pi knows to send packets back to the RouterBoard and NOT direct to the ISP router, which would drop them), we also need the dreaded ["hairpin" SNAT rule](https://help.mikrotik.com/docs/spaces/ROS/pages/3211299/NAT#NAT-HairpinNAT) *as well*!
#### Hairpin - a deep dive
I struggled for a long time after implementing only the first 2 rules in the list above, which seemed to half-work, so I just thought I was missing some vital detail in those rules. Little did I realise a _whole extra rule_ was needed. But here's what happens without the hairpin - follow the arrows!:
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
service ispr(affcircle:c-firewall-green) [ISP Router] in home
group mik(affcircle:c-router)[RB2011] in home
service gbe(affcircle:c-switch-green) in mik
service pi(affcircle:c-server-green)[Pi] in home
internet:B --> T:ispr
ispr:B --> T:gbe
gbe:R --> L:pi
pi:T --> R:ispr
```
The pi, seeing traffic coming from (what appears to be) the ISP router due to the DST-NAT being employed by the RB2011, does the logical thing and addresses its responses to it. But the ISP router _isn't expecting anything from the Pi_ - it was told to port-forward to the Mikrotik - so it drops the responses. When you add the hairpin rule, it causes the packets to stop using that "roundabout" path and tricks the destination server into taking the exact same return path (hence the name):
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
service ispr(affcircle:c-firewall-green) [ISP Router] in home
group mik(affcircle:c-router)[RB2011] in home
service gbe(affcircle:c-switch-green) in mik
service pi(affcircle:c-server-green)[Pi] in home
internet:B --> T:ispr
ispr:B <--> T:gbe
gbe:R <--> L:pi
```
And so we end up with:
### Allow dst-NATted port 80 traffic to the Pi
```
/ip/firewall/filter/add
chain=forward
action=accept
connection-nat-state=dstnat
protocol=tcp
dst-address=10.240.0.200
dst-port=80
comment="allow port 80 dstnatted to pi to cross router"
```
### dst-NAT incoming port 8888 to Pi port 80
```
/ip/firewall/nat/add
chain=dstnat
action=dst-nat
to-addresses=10.240.0.200
to-ports=80
protocol=tcp
dst-address=10.240.0.11
dst-port=8888
comment="port-forward 8888 from ISP to pi:80"
```
### "Hairpin" the port-forwarded Pi traffic back via the RB
```
/ip/firewall/nat/add
chain=srcnat
action=masquerade
protocol=tcp
dst-address=10.240.0.200
comment="hairpin for the pi port fwd"
```
#### Testing
For testing I find the `nc` command-line tool is a lot quicker to give the "I got through" feedback than a browser, which usually will have lots of retry mechanisms making things less black-and-white.
On the server (`pi`), after stopping anything already on port 80 (e.g. `sudo service lighttpd stop`):
```
% sudo nc -vlk 80
Listening on [0.0.0.0] (family 2, port 80)
```
On a random client inside the `Home` network:
```
% nc -v mikrotik 8888
Connection to mikrotik port 8888 [tcp/ddi-tcp-1] succeeded!
```
#### API Control
And once all that is in place, we can control it atomically by simply disabling the `forward` filter rule until we need it.
The RouterOS API is not particularly-well documented and nor is it particularly consistent in its operations, but luckily some excellent Internet citizen has prepared [a Postman collection](https://tikoci.github.io/restraml/routeros-openapi3.json) that perfectly captures all of the weird-and-wonderful quirks. Fortunately, the API to enable an existing firewall filter rule is dead simple:
```
POST /ip/firewall/filter/enable
Payload: {
"numbers": "a list of the firewall filter rule ids to enable"
}
```
So for our particular situation, where the firewall filter list shows:
```
/ip/firewall> /ip/firewall/filter/print
Flags: X - disabled, I - invalid; D - dynamic
0 D ;;; special dummy rule to show fasttrack counters
chain=forward action=passthrough
1 ;;; allow 10.248/13 to be forwarded(see also the NAT tab)
chain=forward action=accept connection-nat-state="" src-address=10.248.0.0/13 log=no log-prefix="allow-13-subnet-to-forward"
2 X ;;; allow port 80 dstnatted to pi to cross router
chain=forward action=accept connection-nat-state=dstnat protocol=tcp dst-address=10.240.0.200 dst-port=80 limit=10,20:packet log=no log-prefix="any-dst-nat"
...
```
... so rule number `2` (the disabled one) is the target. As a `curl` command, it'll be:
```
curl -X POST -s -k -u apiuser:apipw \
http://mikrotik/rest/ip/firewall/filter/enable \
--data '{ "numbers" : "2" }' \
-H "content-type: application/json"
```
and to disable is exactly the same with the word `disable` in place of `enable`. We're finally ready to automate!
# Putting it all together
First let's see the state of the certificate we want to renew:
```
% sudo certbot certificate
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: my.example.com
Domains: my.example.com
Expiry Date: 2025-08-28 04:02:03+00:00 (VALID: 60 days)
Certificate Path: /etc/letsencrypt/live/my.example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/my.example.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```
Let's put some suitable commands into a Bash script that will be executed as a `--pre-hook`:
`/home/pi/lets-encrypt-pre-renewal-tasks.sh`:
```
#!/bin/bash
echo "Stopping Lighttpd"
service lighttpd stop
echo "Stopped Lighttpd"
echo "Enabling port 80 forwarding to Pi"
curl -X POST -s -k -u apiuser:apipw \
http://mikrotik/rest/ip/firewall/filter/enable \
--data '{ "numbers" : "2" }' \
-H "content-type: application/json"
echo "Port 80 forward enabled"
```
... and similarly, but reversed, in `/home/pi/lets-encrypt-post-renewal-tasks.sh`.
Let's try it in `--dry-run` mode and forcing it to renew even though it's early:
```
% sudo certbot renew \
--pre-hook="/home/pi/lets-encrypt-pre-renewal-tasks.sh" \
--post-hook="/home/pi/lets-encrypt-post-renewal-tasks.sh" \
--force-renewal
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/my.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Plugins selected: Authenticator standalone, Installer None
Running pre-hook command: /home/pi/lets-encrypt-pre-renewal-tasks.sh
Output from lets-encrypt-pre-renewal-tasks.sh:
Stopping Lighttpd
Stopped Lighttpd
Enabling port 80 forwarding to Pi
[]Port 80 forward enabled
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for my.example.com
Waiting for verification...
Cleaning up challenges
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/my.example.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/my.example.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Running post-hook command: /home/pi/lets-encrypt-post-renewal-tasks.sh
Output from lets-encrypt-post-renewal-tasks.sh:
Disabling port 80 forwarding to Pi
[]Port 80 forward disabled
Starting Lighttpd
Started Lighttpd
```
Magic!
Now we can just add this to our Pi's `crontab` to run monthly, and let certbot itself decide if it actually needs to take any action:
```
# Attempt to renew the cert at 7:11am on the 13th and 26th of each month:
11 7 13,26 * * certbot renew --pre-hook="/home/pi/lets-encrypt-pre-renewal-tasks.sh" --post-hook="/home/pi/lets-encrypt-post-renewal-tasks.sh"
```
Friday, 13 June 2025
Mikrotik RouterBoard - Kid Control
[Kid Control](https://help.mikrotik.com/docs/spaces/ROS/pages/129531911/Kid+Control) is Mikrotik's name for a parental control layer offered in RouterOS. I'm currently using it as a defense-in-depth/belt-and-braces approach to ensure the kids' screen time isn't getting excessive, as unfortunately it seems Apple's Screen Time and/or app/device restrictions just cannot be relied upon.
They also give a quick *additional* level of restriction that can be called upon as a threat/persuasion to GET OFF THAT PHONE - "cutting off 'the wifi'" is a nuclear option I'll admit, but it can get results.
There were a few additional complications setting Kid Control up on my network however, as it's *entirely predicated upon the Mikrotik device being the default internet gateway for the kids' devices* - i.e. being the "ISP router". On my network, the RB2011 does not play that role; if we refer to a cut-down version of my network diagram, just showing the relevant "infrastructure":
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
group study[Study] in home
service ispr(affcircle:c-firewall) [ISP Router] in study
group mik(affcircle:c-router)[RB2011] in study
service gbe(affcircle:c-switch-blue)[GbE] in mik
service hun(affcircle:c-switch-green)[100Mbps] in mik
service studywifi(affcircle:c-wireless-green)[WiFi AP] in study
group closet[Closet] in home
group closetap(affcircle:c-switch) [ClosetAP] in closet
service closetswitch(affcircle:c-switch-blue)[Switch] in closetap
service closetwifi(affinity:wifi)[WiFi] in closetap
internet:B -- T:ispr
ispr:L -[GbE]- R:gbe
ispr:R -[GbE study to closet backbone]- L:closetswitch
gbe:L -- R:hun
junction huns in study
hun:L -- R:huns
huns:B -[100Mbps]- T:studywifi
```
Up until now, every networked device has been handed a DHCP lease (or been manually configured) with the "ISP Router"'s IP address as its Default Gateway. The change I have now made (after dogfooding with my own devices for a while) is that "kid-controllable" devices now get handed a slightly different DHCP lease (adjusted on a one-by-one basis from the RouterBoard web UI) that specifies the RB2011's own IP address as the default gateway:
Where `use-me-as-your-default-gateway` is just a __DHCP Option__ as follows:
And the RB2011 _itself_ has been configured with a default route to the ISP Router:
## Creating a blockable path through the RouterBoard
But unfortunately that's *not enough*. Out of the box, the RouterBoard simply *will not* forward packets from those specially-configured (aka "kid") devices. So some firewall configuration is needed. While this is all completely logical, and [beautifully explained](https://help.mikrotik.com/docs/spaces/ROS/pages/250708066/Firewall) in the [Mikrotik documentation](https://help.mikrotik.com/docs/spaces/ROS/pages/328227/Packet+Flow+in+RouterOS), it took a while to figure out...
### Special addresses for the special devices
I've [long been a proponent](https://blog.themillhousegroup.com/2011/04/ultimate-ubuntu-build-server-guide-part_25.html) of using a `10.a.b.c`-style network for the extra degrees of freedom those `a`,`b` and `c` bytes give you. Now here's a case where it nicely pays off. Eagle-eyed readers will have already spotted that the `john-iphone` DHCP lease shown above was assigned the IP address `10.248.0.110` whereas other equipment on my network lives at `10.240.0.x`. That extra bit:
```
10.240d = 00001010 11110000
10.248d = 00001010 11111000
```
... combined with a bit-mask of `/13`, i.e. the leftmost 13 bits in `10.248`, give us a telltale for "kid" devices that need special treatment, namely:
### Allow their traffic to be forwarded across the router
Both these firewall rules are dead simple, but they need to be there, so I'm just showing them in the Firewall summary view as there is literally nothing else to them.
This first rule just says that if a packet arrives from one of those `10.248.0.0/13` devices, and it's *not destined for the Mikrotik itself* (i.e. the chain is `forward`, not `input`) then `accept` it rather than continuing firewall rule processing, which would eventually hit the last rule (not shown), and throw the packet away.
### Allow their traffic to be forwarded across the router
The second rule is more subtle but just as necessary. Without it, packets from kid devices addressed to the wider Internet will be forwarded "over" the Mikrotik, sent to the ISP Router due to the default route in the Mikrotik, head to the Internet due to the default route in the ISP Router, and response packets make their way back via the same path due to the magic of NAT - *except for the final hop* back to the original kid device, because the Mikrotik doesn't know what to do with the response.
So we configure a "Source-NAT" rule for our kid devices, that says that the RB2011 will "masquerade" on behalf of them; and thus it will know what to do with those response packets when they arrive back from the internet - send them back to the original kid device:
... And now that we've got packets flowing across the router, Kid Control rules can be layered on top to stop them again 😂
Friday, 2 May 2025
Affinity-for-mermaid
A little aside from the [Mikrotik series](https://blog.themillhousegroup.com/search/label/mikrotik), but related.
As this blog series starts getting a little more involved I wanted to include some network diagrams. [Mermaid.js](https://mermaid.js.org/) has been [my choice for diagrams-as-markdown for ages now](https://blog.themillhousegroup.com/2023/10/markdown-and-mermaid-on-blogger-in-2023.html), and I noted that the new(ish) beta [architecture syntax](https://mermaid.js.org/syntax/architecture.html) would be a good fit.
Notably lacking however, was a nice suite of standard network diagram icons (routers, switches, firewalls, servers etc) to go with it.
A quick search led me to a Reddit question in [`r/networking`](https://www.reddit.com/r/networking/comments/108buv8/what_network_diagram_icons_is_everyone_using/?rdt=63267) where the [Affinity SVG set](https://github.com/ecceman/affinity) by [ecceman on GitHub]((https://github.com/ecceman) was [highly recommended](https://www.reddit.com/r/networking/comments/108buv8/comment/j3r8wxz/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button).
I liked the style, and the fact that the icons could be had "naked" as well as surrounded by circles or rounded squares (and in colour).
One [fork](https://github.com/themillhousegroup/affinity-for-mermaid) and some processing with [IconTools](https://iconify.design/docs/libraries/tools/import/directory.html) has led to a [Mermaid.js-compatible icon library](https://themillhousegroup.github.io/affinity-for-mermaid/), ready to consume with just a couple of extra lines of Mermaid configuration:
```
mermaid.registerIconPacks([{
name: 'affinity',
loader: () => fetch('https://themillhousegroup.github.io/affinity-for-mermaid/icons.json').then((res) => res.json()),
}]);
```
In addition to hosting the all-important JSON files, the GitHub pages also include [icon list](https://themillhousegroup.github.io/affinity-for-mermaid/demo.html) and [demo](https://themillhousegroup.github.io/affinity-for-mermaid/demo.html) pages so you can see what's available:
I have plans to expand this list to show the circular and square variants (and fix up some of the naming inconsistencies therein) - I might even see if I can code up a live sandbox page too ...
But without any further ado, here's the Mermaid architecture description for wired part of my home network ...
```
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
group study[Study] in home
service ispr(affcircle:c-firewall) [ISP Router] in study
group mik(affcircle:c-router)[RB2011] in study
service gbe(affcircle:c-switch-blue)[GbE] in mik
service hun(affcircle:c-switch-green)[100Mbps] in mik
service pi(affcircle:c-server-blue)[Pi] in study
service nas(affcircle:c-nas-blue)[NAS] in study
service nuc(affcircle:c-server-green)[NUC] in study
service studywifi(affcircle:c-wireless-green)[WiFi AP] in study
group closet[Closet] in home
group closetap(affcircle:c-switch) [ClosetAP] in closet
service closetswitch(affcircle:c-switch-blue)[Switch] in closetap
service closetwifi(affinity:wifi)[WiFi] in closetap
service tv(affcircle:c-client-blue)[TV] in home
service ps4(affcircle:c-server-blue)[PS4] in home
service cc(affcircle:c-client-blue)[ChromeCast] in home
internet:B -- T:ispr
ispr:L -[GbE]- R:gbe
ispr:R -[GbE study to closet backbone]- L:closetswitch
gbe:L -- R:hun
junction huns in study
hun:L -- R:huns
huns:L -[100Mbps]- R:nuc
huns:B -[100Mbps]- T:studywifi
junction gbes in study
gbe:B -- T:gbes
gbes:L -[GbE]- R:pi
gbes:R -[GbE]- L:nas
junction avgear in home
closetswitch:B -[GbE]- T:avgear
avgear:L -- R:tv
avgear:B -- T:ps4
avgear:R -- L:cc
```
... and the corresponding diagram, which your browser is rendering as I've enhanced my [md-in-blogger](https://github.com/themillhousegroup/md-in-blogger) script as used by this blog:
```mermaid
architecture-beta
service internet(affinity:cloud)
group home(affinity:house)[Home]
group study[Study] in home
service ispr(affcircle:c-firewall) [ISP Router] in study
group mik(affcircle:c-router)[RB2011] in study
service gbe(affcircle:c-switch-blue)[GbE] in mik
service hun(affcircle:c-switch-green)[100Mbps] in mik
service pi(affcircle:c-server-blue)[Pi] in study
service nas(affcircle:c-nas-blue)[NAS] in study
service nuc(affcircle:c-server-green)[NUC] in study
service studywifi(affcircle:c-wireless-green)[WiFi AP] in study
group closet[Closet] in home
group closetap(affcircle:c-switch) [ClosetAP] in closet
service closetswitch(affcircle:c-switch-blue)[Switch] in closetap
service closetwifi(affinity:wifi)[WiFi] in closetap
service tv(affcircle:c-client-blue)[TV] in home
service ps4(affcircle:c-server-blue)[PS4] in home
service cc(affcircle:c-client-blue)[ChromeCast] in home
internet:B -- T:ispr
ispr:L -[GbE]- R:gbe
ispr:R -[GbE study to closet backbone]- L:closetswitch
gbe:L -- R:hun
junction huns in study
hun:L -- R:huns
huns:L -[100Mbps]- R:nuc
huns:B -[100Mbps]- T:studywifi
junction gbes in study
gbe:B -- T:gbes
gbes:L -[GbE]- R:pi
gbes:R -[GbE]- L:nas
junction avgear in home
closetswitch:B -[GbE]- T:avgear
avgear:L -- R:tv
avgear:B -- T:ps4
avgear:R -- L:cc
```
I won't lie, it took a fair bit of tweaking to get that to lay out properly (there's no way to hint or influence the layout at this stage unfortunately, so trial-and-error via the `T|R|B|L` specifier on edges, and the occasional superfluous [`junction`](https://mermaid.js.org/syntax/architecture.html#junctions) is the only option) - but I'm pretty happy with the result now.
Tuesday, 22 April 2025
Upgrading to the Mikrotik RouterBoard RB2011, Part 3 - DNS
This is Part 3 of my RB2011 series - if you want to start from the start, [here's Part 1](https://blog.themillhousegroup.com/2025/01/upgrading-to-mikrotik-routerboard.html). You can find the [whole series here](https://blog.themillhousegroup.com/search/label/routerboard).
We're part-way through replacing the `dnsmasq` instance on the Raspberry Pi (which does DHCP, DNS and ad-blocking via Pi-Hole-style `0.0.0.0` resolution).
To minimise interruption to network users, we're doing this in a staged manner, gradually moving services over to the RB2011. DHCP is [done](https://blog.themillhousegroup.com/2025/03/upgrading-to-mikrotik-routerboard.html) so we now have the Routerboard providing leases to everyone, but the lease points back to the Pi for DNS. We'll set up DNS now, because _local-device_ name resolution will actually be in a bit of a mess (i.e. it won't work at all) - let's fix that!
### DNS on RouterOS v7
The [Mikrotik DNS server](https://help.mikrotik.com/docs/spaces/ROS/pages/37748767/DNS) is pretty straightforward to configure. All we have to do is set the upstream DNS server (the ISP router) and allow external connections:
```
[admin@MikroTik] > /ip/dns/set servers=10.240.0.1
[admin@MikroTik] > /ip/dns/set allow-remote-requests=yes
```
Then we change our global DHCP server config to start serving the RB2011 as the DNS server:
```
[admin@MikroTik] > /ip/dhcp-server/network/set numbers=0 dns-server=10.240.0.11
```
We should also add a manual entry for the Raspberry Pi, which used to "just know" its own address but now we need to be explicit:
```
[admin@MikroTik] > /ip/dns/static/add name=pi1 address=10.240.0.200 ttl=9000
```
And also, the Pi needs to be told to use the RB2011 for DNS now:
*`/etc/resolv.conf`:*
```
# Use the Mikrotik RouterBoard RB2011:
nameserver 10.240.0.11
```
Whew!
As DHCP clients start getting their new DNS settings (or you can force-renew one if you're impatient) you'll start to see the _IP_ -> _DNS_ -> _Cache_ page in the web UI start to fill up. Mostly with absolute crap, but that's t'Internet for you...
### Adlist functionality
For me, the key advantage of running my own DNS server is ad-blocking via the Pi-Hole or ["DNS sinkhole"](https://en.wikipedia.org/wiki/DNS_Sinkhole) mechanism - in short, DNS requests for known ad-serving domains get the answer `0.0.0.0` which stops resolution in its tracks. RouterOS has full support for this, and it even uses the same format for the blocked-domain list as [Pi-Hole](https://docs.pi-hole.net/database/gravity/#adlist-table-adlist) so the transition from the Pi couldn't be easier.
I like to use my own "curated" adlist as _sometimes_ it's useful to be able to comment-out a line, when it turns out certain extremely-annoying apps actually *need* access to one of those spammy domains. To do this, _push_ your adlist file from the Pi onto the RB2011 using `scp`:
```
pi $ scp /etc/dnsmasq.blockfiles/dnsmasq.blocklist.txt admin@mikrotik:adlist.txt
```
And enable it in the RB2011:
```
[admin@MikroTik] > /ip/dns/adlist/add file=adlist.txt
```
And that's it! Allow some time for all the DHCP clients to get told about their new DNS settings, and then we can turn off `dnsmasq` on the Pi for good!
```
pi $ sudo service dnsmasq stop
```
P.S. don't forget to `/system/backup/save` again on the RB2011!
Sunday, 30 March 2025
Upgrading to the Mikrotik RouterBoard RB2011, Part 2 - DHCP
This is Part 2 of my RB2011 series - [here's Part 1](https://blog.themillhousegroup.com/2025/01/upgrading-to-mikrotik-routerboard.html). You can find the [whole series here](https://blog.themillhousegroup.com/search/label/routerboard).
## DNSMasq Replacement
First, let's replace the `dnsmasq` instance on the Raspberry Pi (which does DHCP, DNS and ad-blocking via Pi-Hole-style `0.0.0.0` resolution). As mentioned before, this will free up the Pi to just be a server again, rather than a critical bit of network infrastructure, but also:
- allow the DHCP pool to be modified from a web UI (easier if using a phone in a pinch)
- make the current DHCP client list visible in a browser
- allow DHCP modifications without having to restart DNSmasq
### DHCP on RouterOS v7
Some other nice properties of the [Mikrotik DHCP server](https://help.mikrotik.com/docs/spaces/ROS/pages/24805500/DHCP#DHCP-DHCPServer) include:
#### Multiple pools with different behaviour
- I like the idea of having "trusted" devices (with a known MAC and fixed IP address) getting better bandwidth, and this is entirely possible with the `rate-limit` property of (leases](https://help.mikrotik.com/docs/spaces/ROS/pages/24805500/DHCP#DHCP-Leases)
- It would be nice to also represent this status with some bit-flagging in the IP address itself, as per my [cunning scheme](https://blog.themillhousegroup.com/2011/04/ultimate-ubuntu-build-server-guide-part_25.html)
#### Event scripting
- you can run a [script upon lease assignment or de-assignment](https://help.mikrotik.com/docs/spaces/ROS/pages/24805500/DHCP#DHCP-DHCPServerProperties)
- could be handy to trigger presence-like automated behaviour
To minimise interruption to network users, we'll do this in a staged manner, gradually moving services over to the RB2011.
### Move to a static IP for the RouterBoard
This is critically important, and if you don't do it right, you're highly-likely to end up with the dreaded unreachable router. Ask me how I know...
To do this in one fell swoop, switch to the Quick Set tab in the top of the web interface. Make the configuration mode __Router__ but leave the Address Acquisition settings alone - we're not going to be using `ETH1`, for our intents and purposes, it's cursed. Then just fill out the Local Network IP settings you'd like the RB2011 to have, but turn off DHCP Server at this stage (we're not ready for that yet) and NAT - our existing gateway device does NAT for us.
Hit *Apply Configuration*, say a small prayer to `${DEITY}` and reload the page (at the new IP address if you changed it). Give it a minute or two before you punt it across the room...
### DHCP Server with single address pool
Next up, we'll set up a DHCP server to do that aspect of what we're currently doing with DNSMasq - a single address pool for unspecified random (a.k.a "guest") devices for which we don't have a mapping, plus a bunch of fixed IPs for known devices. For a smooth switchover, first we'll bring the lease times of everyone getting leases from the Pi *down* to a short interval, to get them roughly "in sync".
Backup the old `dnsmasq.conf` on the Pi:
```
% cd /etc
% sudo cp dnsmasq.conf dnsmasq.conf.pre-rb2011
```
Bring the lease time DOWN for everyone, by going into `dnsmasq.conf` and changing the `dhcp-range` (for random/guest devices) and each `dhcp-host` entry (for known devices) like this:
```
dhcp-range=10.240.0.64,10.240.0.96,1h
...
dhcp-host=00:11:22:33:86:7e,somebox,10.240.0.100,2h
```
to
```
dhcp-range=10.240.0.64,10.240.0.96,5m
...
dhcp-host=00:11:22:33:86:7e,somebox,10.240.0.100,5m
```
Restart dnsmasq for the changes to take effect:
```
pi % sudo service dnsmasq restart
```
Now over on the RB2011, we can run the built-in DHCP Server "wizard" from the SSH prompt:
```
[admin@MikroTik] > /ip/dhcp-server/setup
Select interface to run DHCP server on
dhcp server interface: bridge
Select network for DHCP addresses
dhcp address space: 10.0.0.0/8
Select gateway for given network
gateway for dhcp network: 10.240.0.1
Select pool of ip addresses given out by DHCP server
addresses to give out: 10.240.0.32-10.240.0.63
Select DNS servers
dns servers: 10.240.0.200
Select lease time
lease time: 1800
[admin@MikroTik] >
```
But then immediately disable it:
```
[admin@MikroTik] > /ip/dhcp-server/print
Columns: NAME, INTERFACE, ADDRESS-POOL, LEASE-TIME
# NAME INTERFACE ADDRESS-POOL LEASE-TIME
0 dhcp1 bridge dhcp_pool1 30m
[admin@MikroTik] > /ip/dhcp-server/disable numbers: 0
```
We can now fine-tune the config at our leisure, and enable it once we're entirely ready.
__Set the `domain` so clients have a FQDN:__
```
[admin@MikroTik] > /ip/dhcp-server/network/set numbers=0 domain=home.themillhousegroup.com
```
__Ensure each client gets an ARP table entry:__
```
[admin@MikroTik] > /ip/dhcp-server/set numbers=0 add-arp=yes
```
### Known MAC addresses
I have a reasonably-long list of device MAC addresses (about 30) that I want to have "stable" IP addresses; this is a "static lease" in Mikrotik-speak.
A typical dnsmasq host line looks like:
```
dhcp-host=00:11:22:33:86:7e,somebox,10.240.0.100,15m
```
Each line becomes (for example):
```
[admin@MikroTik] > /ip/dhcp-server/lease/add mac-address=00:11:22:33:86:7e comment=somebox address=10.240.0.100 lease-time=900
```
But note the `hostname` from DNSMasq has just become a comment. To get the nice behaviour that DNSMasq gives us where a DNS entry is created, we need to also add a DNS entry:
```
[admin@MikroTik] > /ip/dns/static/add name=somebox address=10.240.0.100 ttl=900
```
30 entries was too much like manual labour for me, so here's a [little shell script in a Gist](https://gist.github.com/themillhousegroup/41fba50b448ba8f10166decbe2fcc890). Feed it your `dnsmasq.conf` and it'll spit out all the configuration lines you'll need to make your Mikrotik work like DNSMasq did; i.e. a static DHCP server lease and a static DNS entry. Paste the output into your RouterOS SSH session, and confirm that you've got a nice list in the UI at _IP_ -> _DNS_ -> _Static_.
Once you're all set, we're going to take down dnsmasq's DHCP and bring up the RouterOS service as close-together as possible. To do this, first you'll need to tell dnsmasq to NOT listen on the usual interface. On my Pi running Raspbian Buster, this is unfortunately not something predictable like `eth1`, but rather something you'll need to copy-paste from the output of `ifconfig`; e.g.:
```
pi $ ifconfig
enxb827ebfdad60: flags=4163 mtu 1500
inet 10.240.0.200 netmask 255.255.255.0 broadcast 10.240.0.255
```
Edit `/etc/dnsmasq.conf`, adding that ethernet identifier to the `no-dhcp-interface` line (if there is one)
```
# If you want dnsmasq to provide only DNS service on an interface,
# configure it as shown above, and then use the following line to
# disable DHCP and TFTP on it.
no-dhcp-interface=lo,enxb827ebfdad60
```
And now it's swap-over time:
```
pi % sudo service dnsmasq restart
```
```
[admin@MikroTik] > /ip/dhcp-server/enable numbers: 0
```
Then watch the Pi's `/var/lib/misc/dnsmasq.leases` get shorter and the Mikrotik's _IP_ -> _DHCP Server_ -> _Leases_ start to fill up!
Don't forget to backup your settings!
The new DHCP responses still point to the Pi for DNS resolution; we'll configure that next time...
Labels:
dhcp,
dnsmasq,
gist,
github,
homeautomation,
markdown-enabled,
mikrotik,
routerboard
Saturday, 22 February 2025
Introducing ... Blitzcore!
My family has become quite addicted to an excellent little card game called [Dutch Blitz](https://en.wikipedia.org/wiki/Dutch_Blitz). It's easy to learn, compact, reasonably quick to play (although the exact length of an entire game can vary *wildly*...) and it's lots of fun.
The only downside is the scoring system, which involves pen-and-paper and certain amount of maths, making the scorekeeper role unappealing to younger players.
To this end, I'm pleased to release [Blitzcore](https://blitzcore.themillhousegroup.com) ("Blitz-Score") to the world, allowing your phone to do the maths for you and present a clear picture of the score [1](#foot-1).
Paintakingly designed to fit onto exactly one phone-sized browser page like [all good mobile-first webapps](https://blog.themillhousegroup.com/2023/08/searching-for-next-spa.html), it uses bright colours that match the game, big easy-to-hit buttons, and just-in-time calculation to ensure the score is always accurate, no matter what you just edited.
Your player name and colour preferences are stored in LocalStorage to make it even quicker to get going next time.
This is my first "big" (not really) webapp in [Svelte 5](https://svelte.dev/) and [SvelteKit](https://svelte.dev/docs/kit/introduction) and it's been a pretty delightful experience. I `push` to my Github private repo and Netlify (free tier) picks it up and publishes to the world in about 45 seconds. The page load time is absolutely stupendous - Svelte/SvelteKit/Vite is absolutely killing it here.
The only trouble I've ever had has been occasionally Svelte *not* reacting to a change when expected, which so-far has been 100% due to my failure to wrap derived values in [the `$derived()` rune](https://svelte.dev/docs/svelte/$derived). It can be quite a pain to track down these things, which seemed to mostly be around manipulating array entries (using `slice` and friends), but I got there in the end. Enjoy!
---
1 To be clear, this is not a version of the game itself - the creators have actually [attempted to do this for mobile devices](https://dutchblitz.com/), which I have never tried ... [⤴︎](#foot-1-src)
Labels:
dev,
dutch-blitz,
markdown-enabled,
netlify,
node.js,
svelte,
typescript
Wednesday, 22 January 2025
Upgrading to the Mikrotik RouterBoard RB2011, Part 1
For my home network, I needed a firewall that offers an API that I can call from elsewhere (on the trusted side of the network). Think "default block ALL", then an API call comes in to temporarily open one port to one IP address for an hour, before reverting back to "block all".
There are precious few network devices that actually offer this capability; at first this seemed surprising but then I guess there's less overlap between _"I configure secure small networks"_ and _"I write backend code"_ than I thought.
Fortunately, Mikrotik's [RB2011UiAS-RM](https://mikrotik.com/product/RB2011UiAS-RM) (RB2011 for short) is an absolute network Swiss Army Knife, capable of a heap of stuff that I've had a long-suffering Raspberry Pi 3B doing - things like DNSMasq (and domain blocking à la Pi-Hole) and WireGuard VPN termination. I'd like to take those tasks off the trusty Pi, leaving it to be a true application server rather than a bit of network infrastructure. Of course there are many devices that can perform these tasks, but the Mikrotik stands above the rest with its [control-plane API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API), which lets me do the dynamic-firewall thing I need, rackmountability (I 3d-printed the ears) and the fact that it is **fanless** and thus completely silent in operation.
_**Aside**: Here's a little tip if you find something a bit niche, but a bit overpriced, on eBay. **Stick a watch on it**. Eventually, the seller will get notified by eBay and prompted to offer *you*, the watcher, a 10% discount. This has been my strategy for low-urgency nerd items for a while and it works a treat._
Here's how I've got it going.
# Finding it
Firstly (and this may just be some leftover configuration on my secondhand device that a factory reset didn't clear out for some reason), I couldn't get the device to respond in *any* way over the network until the following conditions were *all* true:
- Port `ETH1` plugged in to my existing network
- IP address allocated to `ETH1` via DHCP
- Port `ETH2` plugged into my laptop (Wifi OFF)
- IP address allocated to laptop via DHCP over that link
This is counter to every bit of documentation (Mikrotik-official or otherwise) I've found online that says the router will factory-reset to `192.168.88.1` and will run a DHCP server to hand out `192.168.88.0/24` IPs to connected clients.
Once I'd discovered (via looking at DNSMasq logs on the Pi) that the router was coming online in this way, it was actually a pleasure to use, as it's always annoying having to flip back-and-forth between multiple networks while setting these things up. Effectively the router's web UI is accessible at whatever IP you want to give its `ETH1` MAC via DHCP, and the rest of the `ETH` ports just come up as a "bridge", making the device feel like a simple 10/100/1000 switch, that happens to have a nice UI.
On the subject of UI, Mikrotik does offer a native control application [(WinBox)](https://mikrotik.com/download) but it's not really needed unless you're having major issues finding your device on the network (but see above for some hints!) as it can do some "neighbour discovery" stuff. For me, my 2012 MacBook Pro is running too ancient a MacOS to even consider it. The Web UI ("`webfig`") plus SSH are easily enough for me. After decades of absolutely awful consumer-grade router web UIs, `webfig` is snappy, modern enough, well-considered and bug-free.
# Upgrade to v7 and Initial setup
The RB2011 has 128Mb of RAM, allowing it to be [upgraded to Version 7 of RouterOS](https://help.mikrotik.com/docs/spaces/ROS/pages/115736772/Upgrading+to+v7) (important, because that's the version in which the REST API becomes available). Going from v6.49.17 to v7 is as easy as going to _System -> Packages -> Check for Updates_ and switching the _Channel_ to `upgrade`. `v7.12.1` shows up (in January 2025 at least) and is just a _Download and Install_ away. From there, we're on the v7 train and can go as bleeding-edge as desired. For me the `stable` channel seemed like a safe bet, so I further updated to `7.17` (January 2025).
### Backup
Once we've done that, we should also start [backing up](https://help.mikrotik.com/docs/spaces/ROS/pages/40992852/Backup) the router config after every successful stage of setup. Log in as `admin` via SSH and just do `[admin@MikroTik] > /system/backup/save`. You can see the file from the *Files* top-level menu in the UI or in the console with `/file print`:
```
[admin@MikroTik] /file> print
# NAME TYPE SIZE LAST-MODIFIED
0 skins directory 1970-01-01 11:00:05
1 pub directory 2019-10-23 12:13:14
2 auto-before-reset.backup backup 44.1KiB 1970-01-01 11:00:06
3 MikroTik-20250122-2026.backup backup 28.2KiB 2025-01-22 20:26:13
[admin@MikroTik] /file>
```
### Turn off unwanted services
RouterOS runs a [number of IP services](https://help.mikrotik.com/docs/spaces/ROS/pages/328229/IP+Services) that we neither want nor need; turning them off can only be of benefit; `telnet` and `ftp` are ancient and insecure; we've already established that `winbox` is surplus to requirements. Eventually, it would be good to only allow the `-ssl` versions of the `www` and `api` services, but we'll leave them for now. `ssh` is always wanted.
So to begin, let's see what we have:
`/ip/service/print`:
```
Flags: X - DISABLED, I - INVALID
Columns: NAME, PORT, CERTIFICATE, VRF, MAX-SESSIONS
# NAME PORT CERTIFICATE VRF MAX-SESSIONS
0 telnet 23 main 20
1 ftp 21 main 20
2 www 80 main 20
3 ssh 22 main 20
4 X www-ssl 443 none main 20
5 api 8728 main 20
6 winbox 8291 main 20
7 api-ssl 8729 none main 20
```
Now we can turn them off:
`/ip/service/disable telnet`
`/ip/service/disable ftp`
`/ip/service/disable winbox`
We can also reduce the maximum number of concurrent sessions to something more realistic. It's probably unimportant, but it shows off a cool feature of the command-line:
```
/ip/service/set max-sessions=3
numbers: 2,3,4,5,7
```
...we can apply the same setting change to all the different services in one go. That's really cool. Here's what we have now:
```
Flags: X - DISABLED, I - INVALID
Columns: NAME, PORT, CERTIFICATE, VRF, MAX-SESSIONS
# NAME PORT CERTIFICATE VRF MAX-SESSIONS
0 X telnet 23 main 20
1 X ftp 21 main 20
2 www 80 main 3
3 ssh 22 main 3
4 X www-ssl 443 none main 3
5 api 8728 main 3
6 X winbox 8291 main 20
7 api-ssl 8729 none main 3
```
**Reminder**: Time to backup again!
Subscribe to:
Posts (Atom)