Adventures in Rolling Your Own Router: Part VI
In my previous post, I set up an nftables
firewall. In this post, I’ll use Ansible to
automate the configuration steps we have already performed.
The Purpose of Infrastructure as Code
So far, everything we have done has been manual steps in vim and the CLI.
This works fine, but is tedious and error prone. I’m a big proponent of
Infrastructure as Code
(IaC). It has countless
advantages in a corporate environment, and I’ve grown so accustomed to it that
it seems to make sense even for my personal network.
Later, I’ll be installing Pi-hole and after reviewing the installer, it looks like it does a lot of stuff I might not want. By defining the router using IaC up to that point, I’ll easily be able to get back to that state if the Pi-hole installer does something I didn’t intend to happen. If I didn’t care to have the IaC definition at the end of the process, I could also accomplish this using VM snapshots. The immediate advantages of having the IaC definition is that I’ll be able to set up the router on real hardware far more quickly (no downtime = happy family) and if I ever want to switch to new hardware I’ll be able to do so quickly and easily.
Setting up Ansible
I’ve only used Ansible a few times, but it was always very straightforward. The documentation, like most documentation from Red Hat, is fantastic.
I started by adding my virtual debrouter to my /etc/ethers and
/etc/hosts files on my existing DNS server. I also created an
/etc/ansible/hosts file on my Mac and added debrouter to the file. Then, I
generated a new RSA key pair on my Mac and installed the public key in the
~/.ssh/authorized_keys file on debrouter. Finally, I set up NOPASSWD for
the sudo group, and I am now able to use Ansible to ping debrouter with
sudo privileges:
ansible all -m ping -u jmp --become
debrouter.crbj.io | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
Building a Playbook
Now, I can go back in time to last week and start translating the manual steps
I took into an Ansible playbook. I’ll start a new file called playbook.yml.
I’ll also fire up a new Debian 10 VM called debrouter2 we can use for testing
the playbook.
The first step is to determine the MAC address of each interface so I can give
them customized names extern0 and intern0. We can do this using
facts.
Let’s assume that on first boot only one of the two interfaces will be
connected to Ethernet and have gotten an IPv4 address via DHCP. This interface
will be our extern0 interface, and we should be able to find it using the
default_ipv4 entry in the ansible_facts object.
In real life, my router hardware actually has three interfaces, so I may end up just supplying the interface names manually.
Note that I’m using set_fact instead of vars because I only want these
filters to be evaluated once.
---
- name: Router Setup
hosts: debrouter2.crbj.io
pre_tasks:
- set_fact:
ifs: "{{ ansible_facts.interfaces | reject('equalto', 'lo') }}"
- set_fact:
external_if: "{{ ansible_facts.default_ipv4.interface }}"
- set_fact:
internal_if: "{{ ifs | reject('equalto', external_if) | first }}"
- set_fact:
external_if_macaddress: "{{ ansible_facts[external_if].macaddress }}"
- set_fact:
internal_if_macaddress: "{{ ansible_facts[internal_if].macaddress }}"
tasks:
- name: Print external interface MAC address
ansible.builtin.debug:
var: external_if_macaddress
- name: Print internal interface MAC address
ansible.builtin.debug:
var: internal_if_macaddress
Now, we need templates for our .link files.
% cat templates/link.j2
[Match]
MACAddress={{ macaddress }}
[Link]
Name={{ ifname }}
Now we can use the template module to automatically place the link files on
the router.
- name: Set external interface name
ansible.builtin.template:
src: templates/link.j2
dest: /etc/systemd/network/10-extern0.link
owner: root
group: root
mode: '0644'
vars:
macaddress: "{{ external_if_macaddress }}"
ifname: "extern0"
- name: Set internal interface name
ansible.builtin.template:
src: templates/link.j2
dest: /etc/systemd/network/20-intern0.link
owner: root
group: root
mode: '0644'
vars:
macaddress: "{{ internal_if_macaddress }}"
ifname: "intern0"
Next, we add some templates for our .network files.
% cat templates/dhcp.network.j2
[Match]
Name={{ ifname }}
[Network]
DHCP=ipv4
% cat templates/static.network.j2
[Match]
Name={{ ifname }}
[Network]
Address={{ address_with_netmask }}
IPForward=true
The templates are again fed to the template module.
- name: Set external network configuration
ansible.builtin.template:
src: templates/dhcp.network.j2
dest: /etc/systemd/network/external.network
owner: root
group: root
mode: '0644'
vars:
ifname: "extern0"
- name: Set internal network configuration
ansible.builtin.template:
src: templates/static.network.j2
dest: /etc/systemd/network/internal.network
owner: root
group: root
mode: '0644'
vars:
ifname: "intern0"
address_with_netmask: "192.168.100.1/24"
Now, we can use the systemd module to disable networking.service and
enable systemd-networkd. I’m going a step further here and actually masking
networking.service so it’s not possible to accidentally enable it later.
- name: Completely disable the default networking.service
ansible.builtin.systemd:
name: networking.service
state: stopped
enabled: no
masked: yes
- name: Enable systemd-networkd.service
ansible.builtin.systemd:
name: systemd-networkd.service
state: started
enabled: yes
masked: no
We also need to restart the host at this point. Check out the final playbook on GitHub to see how this is done.
Now we need to install nftables and remove iptables.
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
- name: Remove iptables
ansible.builtin.apt:
name: iptables
purge: yes
state: absent
- name: Install nftables
ansible.builtin.apt:
name: nftables
state: latest
- name: Enable nftables.service
ansible.builtin.systemd:
name: nftables.service
state: started
enabled: yes
masked: no
Finally, we can install the firewall rules using another call to the
template module.
- name: Set up nftables rules
ansible.builtin.template:
src: templates/nftables.conf.j2
dest: /etc/nftables.conf
owner: root
group: root
mode: '0644'
vars:
internal_ifname: "intern0"
external_ifname: "extern0"
lan_network: "192.168.100.0/24"
At this point, we should have a working router. In the next post, I’ll attempt to install Pi-hole without messing up anything we have done so far.