Linux
DIY Linux Router - Part 4 - Podman and Unbound
This is the fourth part of a multi-part series describing how to build your own Linux router.
- Part 1: Initial Setup
- Part 2: Network and Internet
- Part 3: Users, Security and Firewall
- Part 5: Wifi
- Part 6: Nextcloud and Jellyfin
- Impermanence Storage
In the previous parts, we installed the operating system, configured the gateway's internet functionality using PPPoE, and made security adjustments by setting up authentication methods and configuring the firewall.
Now, it's time to install Podman, a drop-in replacement for Docker with some interesting features, and configure Unbound to run on it.
AI-Generated image by Google's Gemini
Table of Contents
About Podman
Since NixOS is configured using .nix
files, it might seem straightforward to install the necessary services directly, without containerization. In many cases, this approach makes sense, as the overhead and complexity of containerization may not always be justified. However, considering the vast number of pre-configured Docker images available that meet our needs, I see no reason not to take advantage of them by using Podman.
Why Podman Instead of Docker?
There are several advantages to using Podman over Docker. While this topic could warrant its article, here are a few key points:
- Daemonless Architecture: Podman does not require a central daemon to run containers. Each container runs as a child process of the Podman command, improving security and reducing the risk of a single point of failure.
- Rootless Containers: Podman allows containers to be run without requiring root privileges, enhancing security by reducing the attack surface.
- Kubernetes Compatibility: Podman can generate Kubernetes YAML files directly from running containers or pods, making it easier to transition from local development to Kubernetes environments.
- Docker-Compatible CLI: Most Docker commands can be used with Podman without modification, making the transition from Docker to Podman seamless.
About Unbound?
Unbound is a local DNS server that caches DNS queries in a local repository, improving DNS resolution times, reducing internet traffic, and slightly increasing internet speed. Additionally, with some scripting, Unbound can function as an ad blocker by blacklisting as many ad-related hosts as possible.
For this project, I'll use a Docker image of Unbound that I created some time ago: cjuniorfox/unbound. This image performs three main functions:
- DNS name resolution.
- Ad-blocking by applying the StevenBlack/hosts list daily.
- Name resolution for the local network by retrieving hostnames from the DHCP Server and assigning them to Unbound's nameserver addresses.
Podman Setup
1. Create the dataset for Podman
Create the intended dataset for Podman. I will do this on the zdata
dataset created at part 1.
ZDATA=zdata
Assuming the data pool is zdata let's create mountpoints considering /mnt/zdata/containers
as the default container path. The idea is to store the Rootfull Containers on /mnt/zdata/containers/root
and for rootless, store on /mnt/zdata/containers/podman
zfs create -o canmount=off ${ZDATA}/containers
zfs create ${ZDATA}/containers/root
zfs create ${ZDATA}/containers/podman
zfs create -o canmount=off ${ZDATA}/containers/root/storage
zfs create -o canmount=off ${ZDATA}/containers/root/storage/volumes
zfs create -o canmount=off ${ZDATA}/containers/podman/storage
zfs create -o canmount=off ${ZDATA}/containers/podman/storage/volumes
chown -R podman:containers /mnt/${ZDATA}/containers/podman
Make sure that the additional zdata
pool is set on hardware-configuration.nix
/etc/nixos/hardware-configuration.nix
...
boot.zfs.extraPools = [ "zdata" ];
...
Let's begin by installing Podman on our NixOS system.
2. Update NixOS Configuration File
Note: Only update the relevant parts of the file. Do not replace the entire file with the content below.
Edit the /etc/nixos/configuration.nix
file:
{ config, pkgs, ... }:
{
...
boot.kernelParams = [ "systemd.unified_cgroup_hierarchy=1" ];
...
imports = [
...
./modules/podman.nix
]
}
Create modules/podman.nix
file. In this file, we have the Podman configuration itself as systemd
user service for starting rootless pods as Podman User.
/etc/nixos/modules/podman.nix
{ pkgs, config, ... }:
{
virtualisation = {
containers.enable = true;
containers.storage.settings = {
storage = {
driver = "zfs";
graphroot = "/mnt/zdata/containers/root/storage";
runroot = "/run/containers/storage";
rootless_storage_path = "/mnt/zdata/containers/$USER/storage";
};
};
podman = {
enable = true;
defaultNetwork.settings.dns_enabled = true;
};
};
environment.systemPackages = with pkgs; [
dive # look into docker image layers
podman-tui # status of containers in the terminal
];
}
4. Create systemd unit to start Podman pods
By default, the Podman installation, install some systemd
units by default, but there are none for dealing with pods properly. There's a systemd
unit for deploying Kubernetes Pods that comes closer to what I need but demands an internet connection right at the moment to start Pods, which there's no way to guarantee during the system initialization. Also, my installation made use of the newer Pasta Network provider, which is great if you compare it to the older slirp4netns, but, at least on my setup, enabling the pod to start with the server gets me an issue because, during the pod's initialization, the Pasta Network is not ready yet, preventing containers to initiate. So, I wrote my parametrized systemd unit to deal with rootless pods, doing two things:
- On
ExecStartPre
, it tries to raise thehello-word
container. If the lack of Pasta Network readiness prevents the container from starting, it waits 2 seconds and then it tries again. - Creates an
ExecStart
andExecStop
receiving the pod name as a parameter.
So, let's write our .nix
file to compose the intended unit service:
/etc/nixos/modules/podman-pod-systemd.nix
{ config, pkgs, ... }:
let
podman = "${config.virtualisation.podman.package}/bin/podman";
logLevel= "--log-level info";
podmanReadness = pkgs.writeShellScript "podman-readness.sh" ''
#!/bin/sh
while ! ${podman} run --rm docker.io/hello-world:linux > /dev/null; do
${pkgs.coreutils}/bin/sleep 2;
done
echo "Podman is ready."
'';
in {
systemd.user.services."podman-pod@" = {
description = "Run podman workloads via podman pod start";
documentation = [ "man:podman-pod-start(1)" ];
wants = [ "network.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
ExecStartPre = "${podmanReadness}";
ExecStart = "${podman} pod ${logLevel} start %I";
ExecStop = "${podman} pod ${logLevel} stop %I";
RemainAfterExit = "true";
};
wantedBy = [ "default.target" ];
};
}
Add the new .nix file to the section imports
from configuration.nix
file.
/etc/nixos/configuration.nix
imports =
[
...
./modules/podman.nix
./modules/podman-pod-systemd.nix
...
];
5. Rebuild the system configuration
To made Podman available, rebuild the system configuration:
nixos-rebuild switch
Unbound Setup
Now that Podman is installed, it's time to set up Unbound. I'll be using the Docker image docker.io/cjuniorfox/unbound. Since Podman supports Kubernetes-like YAML deployment files, we'll create our own based on the example provided in the GitHub repository for this image, specifically in the Kubernetes folder. We'll also set up as rootless for security reasons. Log out from the server and log in as the podman
user. If you set your ~/.ssh/config
as I did, it's just:
ssh router-podman
1. Create Directories and Volumes for Unbound
First, create a directory to store Podman's deployment YAML files and volumes. In this example, I'll create the directory under /home/podman/deployments
and place an unbound.yaml
inside it. Additionally, create the container volume unbound-conf
to store extra configuration files.
mkdir -p /home/podman/deployments/
podman volume create unbound-conf
2. Build the YAML Deployment File
Next, create a unbound.yaml
file in /home/podman/deployments/unbound/
. This file is based on the example provided in the Docker image repository cjuniorfox/unbound.
/home/podman/deployments/unbound.yaml
apiVersion: v1
kind: Pod
metadata:
name: unbound
labels:
app: unbound
spec:
automountServiceAccountToken: false
containers:
- name: server
image: docker.io/cjuniorfox/unbound:1.20.0
resources:
limits:
memory: 200Mi
ephemeral-storage: "1Gi"
requests:
cpu: 0.5
memory: 100Mi
ephemeral-storage: "500Mi"
env:
- name: DOMAIN
value: "home.example.com" # The same as defined on DHCP server section of network.nix
ports:
- containerPort: 53
protocol: UDP
hostPort: 1053
volumeMounts:
- name: unbound-conf-pvc
mountPath: /unbound-conf
restartPolicy: Always
volumes:
- name: unbound-conf-pvc
persistentVolumeClaim:
claimName: unbound-conf
4. Additional Configuration Files
Hosts with fixed IP, fixed leases, and their own Router identification itself can be placed on a customized configuration file that makes the DNS Server return properly DNS queries about. Let's put this configuration file into the newly created volume unbound-conf
. You will find its path at /mnt/zdata/containers/podman/storage/volumes/unbound-conf/_data/
/mnt/zdata/containers/podman/storage/volumes/unbound-conf/_data/local.conf
server:
private-domain: "example.com."
local-zone: "macmini.home.example.com." static
local-data: "macmini.home.example.com. IN A 10.1.78.1"
local-data: "macmini.home.example.com. IN A 10.30.17.1"
local-data: "macmini.home.example.com. IN A 10.90.85.1"
5. Start the unbound pod and check its status
With everything set, start the Unbound Pod with the following command:
podman kube play --log-level info --replace /home/podman/deployments/unbound.yaml
Check it status by doing:
podman pod logs -f unbound
You can also check if DNS queries are being properly processed by doing:
dig @localhost -p 1053 google.com
; <<>> DiG 9.18.28 <<>> @localhost -p 1053 google.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64081
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;google.com. IN A
;; ANSWER SECTION:
google.com. 48 IN A 142.250.79.46
;; Query time: 0 msec
;; SERVER: ::1#1053(localhost) (UDP)
;; WHEN: Thu Nov 14 17:47:19 -03 2024
;; MSG SIZE rcvd: 55
6. Enable Systemd Service for Unbound
It's time to make use of the systemd
unit created above, by enabling our Pod startup during system initialization. Do the following command:
systemctl --user enable --now [email protected]
You can reboot the machine to see if the service starts up with no issues
systemctl --user status [email protected]
[email protected] - Run podman workloads via podman pod start
Loaded: loaded (/home/podman/.config/systemd/user/[email protected]; enabled; preset: enabled)
Active: active (exited) since Thu 2024-11-14 16:48:04 -03; 1h 2min ago
...
Firewall Rules
By default, Linux does not allow opening ports lower than port 1024 as rootless. As the default DNS port is 53, We have to forward port 1053 to 53.
Edit the nftables.nft
file by adding the following:
Open port
/etc/nixos/modules/nftables.nft
table
...
table inet filter {
...
chain unbound_dns_input {
iifname {"br0", "vlan30", "vlan90" } udp dport 1053 ct state { new, established } counter accept comment "Allow Unbound DNS server"
}
...
chain input {
...
jump unbound_dns_input
...
}
}
NAT Redirect
/etc/nixos/modules/nftables.nft
table
...
table nat {
chain unbound_redirect {
# Redirect all DNS requests to any host to Unbound
iifname "br0" udp dport 53 redirect to 1053
# Redirect DNS to unbound, allow third-party DNS servers
ip daddr {10.30.17.1, 10.90.85.1 } udp dport 53 redirect to 1053
}
...
chain prerouting {
...
jump unbound_redirect
}
}
Rebuild NixOS
nixos-rebuild switch
Reload Unbound Pod
Every time Firewall rules are reloaded, is good to reload Pods, so they can reconfigure the expected forward ports.
Run as podman
user:
systemctl --user restart unbound.service
Conclusion
In this part of the series, we successfully installed Podman as a container engine and configured Unbound to run within it, providing DNS resolution and ad-blocking capabilities for our network. By leveraging Podman, we benefit from a more secure, rootless container environment while still utilizing the vast ecosystem of pre-configured Docker images. Additionally, we set up firewall rules to ensure that all DNS traffic is routed through our Unbound server, further enhancing the security of our network.
Next, we will configure our wireless network using a Ubiquiti UniFi AP.
- Part 5: Wifi
keywords: macmini • router • linux • nixos • pppoe • unbound • podman • docker