07 Apr

Manage Wireguard users using Ansible

Day 16 of lockdown here in Haryana due to Covid19. Time for some distraction.


Last week it was reported that Wireguard will be added in next version of Linux kernel. I have been using Wireguard from over a year and it has been working great. I replaced OpenVPN with Wireguard for both site to site VPN as well as client-server VPN. If you are looking for a free open source VPN for remote employees or just connecting to your own remote servers Wireguard can be a really good candidate.

Recently I create client-server VPN at home so that I can get inside the home network whenever travelling (which is little uncommon due to Covid19 lockdown!).

Somehow I did not find any good automated script to generate keys. Tried a few projects and either they did not work or they tend to re-write everything inside /etc/wireguard directory. I presently run 5 different VPN daemons on my Raspberry Pi. It does site to site VPNs to two locations over two different uplinks and then OSPF running over FRR takes care of dynamically routing. For 5th one which is client-server VPN, I used Ansible put a playbook. Idea is to run playbook each time I want to add a user, provide it with client-name and client-ip (didn’t automate client IP since it’s just 4-5 devices max) and the playbook will take care of generating keys, config (which can be copy-pasted in Wireguard running on a laptop) and also QR code which can be scanned for importing config along with the keys in iOS devices. Ideally, I should put a more detailed one as Ansible role but then it’s just me being lazy and settling for a playbook instead.

Here’s goes the playbook!

---
  - hosts: ## Put server hostname here ##
    gather_facts: no
    become: yes
    vars:
      client_name: anurag-phone
      client_ip: 10.0.0.10
      client_mask: 24
      client_dns: 10.1.0.5
      wgname: wg5
      wgport: 5005
      work_dir: "/home/anurag/config"
      server_ip: ## Put server IP here ##
    tasks:
      - name: Ensure {{ work_dir }} exists
        file:
          path: '{{ work_dir }}'
          state: directory
      - name: Generate client keys for {{ client_name }}
        shell:
          cmd: wg genkey | tee privatekey | wg pubkey > publickey
          chdir: "{{ work_dir }}"
      - name: Read client privatekey and register into variable
        shell: cat {{ work_dir }}/privatekey
        register: privatekey
      - name: Read client publickey and register into variable
        shell: cat {{ work_dir }}/publickey
        register: clientpublickey
      - name: Read server publickey of server and register into variable
        shell: cat /etc/wireguard/publickey
        register: serverpublickey
      - name: Add {{ client_name }} to the server
        blockinfile:
          path: '/etc/wireguard/{{ wgname }}.conf'
          marker: "## Added by Ansible"
          block: |
              # {{ client_name }}
              [Peer]
              PublicKey = {{ clientpublickey.stdout }}
              AllowedIPs = {{ client_ip }}/32
      - name: Stop wireguard for {{ wgname }}
        command: wg-quick down {{ wgname }}
        register: wireguardstop
        tags: wireguardrestart
      - debug:
          var: wireguardstop.stderr_lines
        tags: wireguardrestart
      - name: Start wireguard for {{ wgname }}
        command: wg-quick up {{ wgname }}
        register: wireguardstart
        tags: wireguardrestart
      - debug:
          var: wireguardstart.stderr_lines
        tags: wireguardrestart
      - name: Generate client config for {{ client_name }} for full internet access
        blockinfile:
          path: "{{ work_dir }}/{{ client_name }}-full.conf"
          block: |
              [Interface]
              PrivateKey = {{ privatekey.stdout }}
              Address = {{ client_ip }}/{{ client_mask }}
              DNS = {{ client_dns }}
              [Peer]
              PublicKey = {{ serverpublickey.stdout }}
              AllowedIPs = 0.0.0.0/0
              Endpoint = {{ server_ip }}:{{ wgport }}
          state: present
          create: yes
      - name: Generate QR code for {{ client_name }}
        shell: qrencode -t ansiutf8  < {{ work_dir }}/{{ client_name }}-full.conf  > {{ work_dir }}/{{ client_name }}-qr-full
        tags: qr

Some limitations of this playbook:

  1. Cannot be used to delete users. I don’t do that often and thus I am OK to delete those just manually though one can make it little more smart to do that. Probably define users within vars and have a check to not-re-write keys during each run.
  2. It will keep on adding keys to the server side config and hence if run twice for same user, IP – it will add junk. Again, this was more of a quick written solution and not a extensively written playbook to tackle that.

The key objective here was just to generate keys, insert client public key in server side config and server’s key in client side config. And ofcourse making config available in text and QR code form so that one can use import and delete it.

06 Apr

Route filter generation for Mikrotik RouterOS via IRR

A while back I posted about routing filter generation via bgpq3 for Cisco (ios and XR) and Juniper JunOS based routers. I have received a number of emails in last few months about automated filter generation for Mikrotik routeros. Since Mikrotik’s CCRs are getting quite popular across small to mid-sized ISPs.
So this blog post is about ways for generating filter config for a given ASN via IRR. One can use such logic with some kind of remote login mechanism like rancid (look for mtlogin here).
I tried building around bgpq3 but it seems more easy with another popular tool in the domain called IRR Power Tools. Once IRR Power Tools (IRRPT) is setup, it allows us to fetch prefixes based via Internet Routing Registries and also aggregates them.
 
So, for instance, let’s pick AS54456:

anurag@tools:~/irrpt$ bin/irrpt_fetch 54456
Processing AS54456 (Record 1)
   - Importing /home/anurag/irrpt/db/54456                   version 1.1
   - Importing /home/anurag/irrpt/db/54456.4                 version 1.1
   - Importing /home/anurag/irrpt/db/54456.6                 version 1.1
   - Importing /home/anurag/irrpt/db/54456.agg               version 1.1
   - Importing /home/anurag/irrpt/db/54456.4.agg             version 1.1
   - Importing /home/anurag/irrpt/db/54456.6.agg             version 1.1
Completed processing of 1 IRR object(s).
anurag@tools:~/irrpt$

 
So now we have got prefixes and this includes both basic route objects as well as aggregates.

anurag@tools:~/irrpt$ cat /home/anurag/irrpt/db/54456.4
199.116.76.0/24
199.116.77.0/24
199.116.78.0/24
199.116.79.0/24
anurag@tools:~/irrpt$
anurag@tools:~/irrpt$ cat /home/anurag/irrpt/db/54456.4.agg
199.116.76.0/22
anurag@tools:~/irrpt$

 
It offers a nice interface for generation of config for Cisco, Juniper, Extreme, Foundry and Force10. Example:

anurag@tools:~/irrpt/bin$ ./irrpt_pfxgen -f cisco 54456
conf t
no ip prefix-list CUSTOMER:54456
no ipv6 prefix-list CUSTOMERv6:54456
ip prefix-list CUSTOMER:54456 permit 199.116.76.0/22 le 24
end
write mem
anurag@tools:~/irrpt/bin$
anurag@tools:~/irrpt/bin$
anurag@tools:~/irrpt/bin$ ./irrpt_pfxgen -f juniper 54456
policy-options {
    replace: policy-statement CUSTOMER:54456 {
        term prefixes {
            from {
                route-filter 199.116.76.0/22 upto /24;
            }
            then next policy;
        }
        then reject;
    }
}
policy-options {
    replace: policy-statement CUSTOMERv6:54456 {
        term prefixes {
            from {
            }
            then next policy;
        }
        then reject;
    }
}
anurag@tools:~/irrpt/bin$

 
So I put a routeros instance in a VM to test and create config from their CLI. Config looks something like this:

[admin@MikroTik] > routing filter add chain=Cloudaccess prefix=199.116.76.0/22 prefix-length=22-24 action=accept
[admin@MikroTik] > routing filter add chain=Cloudaccess  action=reject
[admin@MikroTik] >

This seems logical and can be scripted. So one can have a script to read the aggregate file and if aggregate says /24 one can put it directly in the filter else allow filter up to /24 from whatever range the pool starts and similar logic in IPv6.
So here’s the script:

#!/bin/bash
# Script for generating BGP filter for Mikrotik RouterOS
# Input name of chain via $1 and ASN via $2
irrpt=/home/anurag/irrpt
# Grab latest filters via RADB / other IRRs using IRRPT
echo "Grabbing prefixes for AS$2"
php $irrpt/bin/irrpt_fetch $2
echo "***Start of Mikrotik routeros config below***"
# IPv4 config part
cat $irrpt/db/$2.4.agg | while read prefix
do
masklength=`echo $prefix|awk -F '/' '{print $2}'`
if [ "$masklength" -eq 24 ]
	then
	# Prefix is a /24 - generating config without defining prefix length
	echo "routing filter add chain=$1-IPv4 prefix=$prefix action=accept"
elif [ "$masklength" -lt 24 ]
	then
	# Prefix is greater than /24 - generating config with prefix length upto /24
	echo "routing filter add chain=$1-IPv4 prefix=$prefix prefix-length=$masklength-24 action=accept"
fi
done
# Last entry for denial of pools
echo "routing filter add chain=$1-IPv4 action=reject"
cat $irrpt/db/$2.6.agg | while read prefix6
do
masklength6=`echo $prefix6|awk -F '/' '{print $2}'`
if [ "$masklength6" -eq 48 ]
	then
	# Prefix is a /48 - generating config without defining prefix length
	echo "routing filter add chain=$1-IPv6 prefix=$prefix6 action=accept"
elif [ "$masklength6" -lt 48 ]
	then
	# Prefix is greater than /48 - generating config with prefix length upto /48
	echo "routing filter add chain=$1-IPv6 prefix=$prefix6 prefix-length=$masklength6-48 action=accept"
fi
done
# Last entry for denial of pools
echo "routing filter add chain=$1-IPv6 action=reject"
echo "***End of Mikrotik routeros config***"

 
So the script works except with a small bug in IPv6 aggregation which is the issue with IRRPT and I have reported same on their GitHub project page here.
 
An example of the script in progress for Cloudaccess AS54456:

anurag@tools:~/irrpt$ ./routeros.sh Cloudaccess 54456
Grabbing prefixes for AS54456
Processing AS54456 (Record 1)
Completed processing of 1 IRR object(s).
***Start of Mikrotik routeros config below***
routing filter add chain=Cloudaccess-IPv4 prefix=199.116.76.0/22 prefix-length=22-24 action=accept
routing filter add chain=Cloudaccess-IPv4 action=reject
routing filter add chain=Cloudaccess-IPv6 action=reject
***End of Mikrotik routeros config***
anurag@tools:~/irrpt$

 
Here’s another example of it in action with NPCI’s AS132351

anurag@tools:~/irrpt$ ./routeros.sh NPCI 132351
Grabbing prefixes for AS132351
Processing AS132351 (Record 1)
   - Importing /home/anurag/irrpt/db/132351                  version 1.1
   - Importing /home/anurag/irrpt/db/132351.4                version 1.1
   - Importing /home/anurag/irrpt/db/132351.6                version 1.1
   - Importing /home/anurag/irrpt/db/132351.agg              version 1.1
   - Importing /home/anurag/irrpt/db/132351.4.agg            version 1.1
   - Importing /home/anurag/irrpt/db/132351.6.agg            version 1.1
Completed processing of 1 IRR object(s).
***Start of Mikrotik routeros config below***
routing filter add chain=NPCI-IPv4 prefix=103.14.160.0/22 prefix-length=22-24 action=accept
routing filter add chain=NPCI-IPv4 action=reject
routing filter add chain=NPCI-IPv6 prefix=2001:df0:2f0::/46 prefix-length=46-48 action=accept
routing filter add chain=NPCI-IPv6 action=reject
***End of Mikrotik routeros config***
anurag@tools:~/irrpt$

 
 

 
 
Thinking to automate? 
The config between ***start*** and ***end*** can be pasted directly in CLI with Mikrotik. I would not recommend using it for manual filtering of any larger network. Automated filtering where filters are generated regularly makes sense but manual filtering without automation can be damaging. One can use a script like this for connecting to smaller networks. Also, IRRPR offers diff management via CVS (I hope they come up with git on that part) and it comes with an option to trigger email update so Network admins can know when to manually update. I would prefer that for non-commit based platforms since with Cisco ios or Mikrotik routeros it can be tricky to auto update prefix list. If one does no ip prefix list before triggering update it will cause a major noticeable impact. So ideal way to manage that on non-commit based devices would be to maintain a list of prefixes separately in the plain text file or a database and diff it against old one & only push for changes. Do-able and should be preferred that way instead of deleting and re-adding the whole list while automating.
 
Time to get back to work! 🙂