Initial commit, squashed

This commit is contained in:
Timotej Lazar 2023-12-18 11:22:14 +01:00
commit 158e8740b8
83 changed files with 2718 additions and 0 deletions

View file

@ -0,0 +1,18 @@
#!/bin/sh
class="${1}"
name="${2}"
state="${3}"
case "${state}" in
"MASTER" | "FAULT")
systemctl start "${name}"
;;
"BACKUP" | "STOP")
systemctl stop "${name}"
;;
*)
logger "keepalived unknown state for ${name}: ${state}"
exit 1
;;
esac

View file

@ -0,0 +1,21 @@
- name: reload frr
command:
cmd: /usr/lib/frr/frr-reload
when: "'handler' not in ansible_skip_tags"
- name: reload interfaces
command:
cmd: ifreload -a
when: "'handler' not in ansible_skip_tags"
- name: restart keepalived
service: name=keepalived state=restarted
when: "'handler' not in ansible_skip_tags"
- name: restart radvd
service: name=radvd state=restarted
when: "'handler' not in ansible_skip_tags"
- name: reload systemd
systemd: daemon_reload=yes
when: "'handler' not in ansible_skip_tags"

2
roles/exit/meta/main.yml Normal file
View file

@ -0,0 +1,2 @@
dependencies:
- role: fabric

26
roles/exit/tasks/dhcp.yml Normal file
View file

@ -0,0 +1,26 @@
- name: Install keepalived
import_tasks: keepalived.yml
- name: Create keepalive notify script for systemd services
copy:
dest: /usr/local/bin/
src: keepalive-service
mode: 0755
- name: Configure DHCP relays
template:
dest: "/etc/default/isc-dhcp-relay"
src: isc-dhcp-relay.j2
- name: Set up keepalived
template:
dest: /etc/keepalived/keepalived.conf
src: keepalived.conf.j2
mode: 0600
notify: restart keepalived
- name: Enable keepalived
service:
name: keepalived
enabled: yes
state: started

View file

@ -0,0 +1,25 @@
# We should just apt install it but it’s broken with Cumulus 5.4 + Debian 10.
- name: Install keepalived
block:
- name: Install deps for keepalived
package:
name: autoconf,automake,build-essential,pkg-config,libxtables-dev,libip4tc-dev,libip6tc-dev,libipset-dev,libnl-3-dev,libnl-genl-3-dev,libssl-dev
- name: Checkout keepalived source
git:
repo: https://github.com/acassen/keepalived
dest: /usr/local/src/keepalived
version: v2.2.7
- name: Build and install keepalived
shell: |
./autogen.sh
./configure --sysconfdir=/etc
make
make install
args:
chdir: /usr/local/src/keepalived
creates: /usr/local/sbin/keepalived
notify: reload systemd
- meta: flush_handlers

26
roles/exit/tasks/main.yml Normal file
View file

@ -0,0 +1,26 @@
- name: Set up networks
template:
dest: /etc/network/interfaces.d/networks.intf
src: networks.intf.j2
mode: 0644
notify: reload interfaces
- name: Set up firewall links
template:
dest: /etc/network/interfaces.d/firewall.intf
src: firewall.intf.j2
mode: 0644
notify: reload interfaces
- name: Set up FRR
template:
dest: /etc/frr/frr.conf
src: frr.conf.j2
mode: 0600
notify: reload frr
- name: Set up radvd
import_tasks: radvd.yml
- name: Set up DHCP relay
import_tasks: dhcp.yml

View file

@ -0,0 +1,36 @@
# We should just apt install it but we need features not in released
# version. Also the cumulus package is two versions behind.
- name: Install radvd
block:
- name: Install deps for radvd
package:
name: autoconf,automake,bison,build-essential,flex,gettext,libtool,pkg-config,libbsd-dev,libbsd0
- name: Checkout radvd source
git:
repo: https://github.com/radvd-project/radvd
dest: /usr/local/src/radvd
version: f67335b5335b6ed5ca68d6fa71c08cccb4f3a629
- name: Build and install radvd
shell: |
./autogen.sh
./configure --without-check
make
make install
args:
chdir: /usr/local/src/radvd
creates: /usr/local/sbin/radvd
- name: Configure radvd
template:
dest: /etc/radvd.conf
src: radvd.conf.j2
mode: 0644
notify: restart radvd
- name: Enable radvd
service:
name: radvd
enabled: true
state: started

View file

@ -0,0 +1,41 @@
{% set exit = inventory_hostname.split('-')[1]|int %}
{% set lo_address = interfaces
| selectattr('name', '==', 'lo')
| map(attribute='ip_addresses') | first
| selectattr('role') | selectattr('role.value', '==', 'loopback')
| map(attribute='address') %}
{% set ip = lo_address | ipv4 | first %}
{% set ip6 = lo_address | ipv6 | first -%}
auto inside
iface inside
vrf-table auto
address {{ ip }}
address {{ ip6 }}
auto outside
iface outside
vrf-table auto
address {{ ip }}
address {{ ip6 }}
{% for iface in ifaces_firewall %}
auto {{ iface }}.2
iface {{ iface }}.2
vrf inside
auto {{ iface }}.4
iface {{ iface }}.4
vrf outside
{% endfor -%}
# Backup firewall routes are exchanged over these subinterfaces.
auto peerlink.2
iface peerlink.2
vrf inside
auto peerlink.4
iface peerlink.4
vrf outside
address {{ "169.254.1.0/24" | ipaddr(exit + 1) }}

View file

@ -0,0 +1,401 @@
{% set lo_address = interfaces | selectattr('name', '==', 'lo')
| map(attribute='ip_addresses') | first
| selectattr('role') | selectattr('role.value', '==', 'loopback')
| map(attribute='address') %}
{% set my_index = inventory_hostname.split('-')[1]|int %}
{% set bridge = interfaces | selectattr('type') | selectattr('type.value', '==', 'bridge') | first %}
{% set my_vlans = bridge.tagged_vlans | sort(attribute='vid') -%}
frr defaults datacenter
log syslog informational
service integrated-vtysh-config
# Route to the outside world.
vrf outside
ip route 0.0.0.0/0 {{ (interfaces | selectattr('name', '==', iface_uplink) | first).custom_fields.gateway.address | ipaddr('address') }} {{ iface_uplink }}
ipv6 route ::/0 fe80::2 {{ iface_uplink }}
# Don’t announce anything at start until we get routes from all our peers.
# Without this packets might get dropped until all routes are synced.
bgp update-delay 10
# Route installation into kernel fails (rarely) without this option.
# It is not documented anywhere and appears to be a Cumulus extension.
zebra nexthop proto only
router-id {{ lo_address | ipv4 | first | ipaddr('address') }}
# Default VRF.
router bgp {{ asn.asn }}
bgp bestpath as-path multipath-relax
neighbor fabric peer-group
neighbor fabric remote-as external
neighbor fabric capability extended-nexthop
neighbor peerlink.4094 interface remote-as external
neighbor peerlink.4094 capability extended-nexthop
neighbor peerlink.4094 bfd 3 150 150
{% for iface in ifaces_fabric %}
neighbor {{ iface }} interface peer-group fabric
neighbor {{ iface }} bfd 3 150 150
{% endfor %}
address-family ipv4 unicast
redistribute connected route-map loopback
neighbor fabric soft-reconfiguration inbound
neighbor fabric route-map fabric->default in
neighbor fabric route-map default->fabric out
import vrf outside
import vrf route-map default-import
exit-address-family
address-family ipv6 unicast
redistribute connected route-map loopback
neighbor fabric activate
neighbor fabric soft-reconfiguration inbound
neighbor fabric route-map fabric->default in
neighbor fabric route-map default->fabric out
import vrf outside
import vrf route-map default-import
exit-address-family
address-family l2vpn evpn
advertise-all-vni
advertise-default-gw
neighbor fabric activate
neighbor peerlink.4094 activate
exit-address-family
# Outside VRF. Direct route to the world, everything else goes to the firewall.
router bgp {{ asn.asn }} vrf outside
bgp bestpath as-path multipath-relax
neighbor peerlink.4 interface remote-as external
neighbor peerlink.4 capability extended-nexthop
neighbor peerlink.4 bfd 3 150 150
neighbor firewall peer-group
neighbor firewall remote-as external
neighbor firewall capability extended-nexthop
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.4 interface peer-group firewall
neighbor {{ iface }}.4 bfd 3 150 150
{% endfor %}
address-family ipv4 unicast
neighbor peerlink.4 soft-reconfiguration inbound
neighbor peerlink.4 route-map peer.4->me in
neighbor peerlink.4 route-map me->peer.4 out
neighbor firewall allowas-in 1
neighbor firewall default-originate
neighbor firewall soft-reconfiguration inbound
neighbor firewall route-map outside->firewall out
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.4 route-map firewall-{{ loop.index }}->outside in
{% endfor %}
redistribute static
redistribute connected route-map loopback-outside
import vrf default
import vrf route-map outside-import
exit-address-family
address-family ipv6 unicast
neighbor peerlink.4 activate
neighbor peerlink.4 allowas-in origin
neighbor peerlink.4 soft-reconfiguration inbound
neighbor peerlink.4 route-map peer.4->me in
neighbor peerlink.4 route-map me->peer.4 out
neighbor firewall activate
neighbor firewall allowas-in 1
neighbor firewall default-originate
neighbor firewall soft-reconfiguration inbound
neighbor firewall route-map outside->firewall out
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.4 route-map firewall-{{ loop.index }}->outside in
{% endfor %}
redistribute static
redistribute connected route-map loopback-outside
import vrf default
import vrf route-map outside-import
exit-address-family
# Inside VRF. Default route via firewall. Direct routes to servers and offices.
router bgp {{ asn.asn }} vrf inside
bgp bestpath as-path multipath-relax
neighbor peerlink.2 interface remote-as external
neighbor peerlink.2 capability extended-nexthop
neighbor peerlink.2 bfd 3 150 150
neighbor firewall peer-group
neighbor firewall remote-as external
neighbor firewall capability extended-nexthop
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.2 interface peer-group firewall
neighbor {{ iface }}.2 bfd 3 150 150
{% endfor %}
address-family ipv4 unicast
neighbor peerlink.2 soft-reconfiguration inbound
neighbor peerlink.2 route-map peer.2->me in
neighbor peerlink.2 route-map me->peer.2 out
neighbor firewall allowas-in 1
neighbor firewall soft-reconfiguration inbound
neighbor firewall route-map inside->firewall out
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.2 route-map firewall-{{ loop.index }}->inside in
{% endfor %}
redistribute connected route-map loopback-inside
{% for vlan in my_vlans %}
import vrf {{ vlan.name }}
{% endfor %}
import vrf default
import vrf route-map inside-import
exit-address-family
address-family ipv6 unicast
neighbor peerlink.2 activate
neighbor peerlink.2 soft-reconfiguration inbound
neighbor peerlink.2 route-map peer.2->me in
neighbor peerlink.2 route-map me->peer.2 out
neighbor firewall activate
neighbor firewall allowas-in 1
neighbor firewall soft-reconfiguration inbound
neighbor firewall route-map inside->firewall out
{% for iface in ifaces_firewall %}
neighbor {{ iface }}.2 route-map firewall-{{ loop.index }}->inside in
{% endfor %}
redistribute connected route-map loopback-inside
{% for vlan in my_vlans %}
import vrf {{ vlan.name }}
{% endfor %}
import vrf default
import vrf route-map inside-import
exit-address-family
{% for vlan in my_vlans %}
# VRF for L2 network {{ vlan.name }}. Imports gateway from inside VRF.
router bgp {{ asn.asn }} vrf {{ vlan.name }}
bgp bestpath as-path multipath-relax
address-family ipv4 unicast
redistribute connected
import vrf inside
import vrf route-map office-import
exit-address-family
address-family ipv6 unicast
redistribute connected
import vrf inside
import vrf route-map office-import
exit-address-family
{% endfor %}
# Prefix lists.
ip prefix-list default permit 0.0.0.0/0
ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32
ipv6 prefix-list fabric permit 2001:1470:fffd:3400::/64 ge 128
{% for vlan in my_vlans %}
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true) %}
{% for prefix in prefixes %}
{% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }} ge 24
{% else %}
ipv6 prefix-list office permit {{ prefix.prefix }} ge 64
{% endif %}
{% endfor %}
{% endfor %}
ip prefix-list vpn permit {{ wg_net | ipaddr('subnet') }}
ip prefix-list nat permit {{ wg_ip | ipaddr('host') }}
{% for network in nat %}
ip prefix-list nat permit {{ network }}
{% endfor %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true, api_filter='role=bgp') | selectattr('tenant') %}
{% if prefix.family.value == 4 %}
ip prefix-list dc permit {{ prefix.prefix }} ge 32
{% else %}
ipv6 prefix-list dc permit {{ prefix.prefix }} ge 64
{% endif %}
{% endfor %}
# Route maps for redistributing own IPs from various VRFs.
route-map loopback permit 1
match interface lo
route-map loopback-inside permit 1
match interface inside
route-map loopback-outside permit 1
match interface outside
# Route maps for importing between VRFs.
route-map default-import permit 10
match ip address prefix-list default
route-map default-import permit 11
match ipv6 address prefix-list default
route-map default-import permit 21
match ipv6 address prefix-list office
route-map default-import permit 30
match ip address prefix-list nat
route-map outside-import permit 10
match ip address prefix-list dc
route-map outside-import permit 11
match ipv6 address prefix-list dc
route-map office-import permit 10
match ip address prefix-list default
route-map office-import permit 11
match ipv6 address prefix-list default
route-map inside-import permit 20
match ip address prefix-list office
route-map inside-import permit 21
match ipv6 address prefix-list office
# Route maps for advertised and received routes.
# Inside ↔ fabric.
route-map default->fabric permit 10
match ip address prefix-list default
route-map default->fabric permit 11
match ipv6 address prefix-list default
route-map default->fabric permit 20
match ip address prefix-list fabric
route-map fabric->default permit 10
match ip address prefix-list fabric
route-map fabric->default permit 20
match ip address prefix-list dc
route-map fabric->default permit 21
match ipv6 address prefix-list dc
# Inside ↔ firewall.
route-map inside->firewall permit 1
match interface lo
route-map inside->firewall permit 20
match ip address prefix-list office
route-map inside->firewall permit 21
match ipv6 address prefix-list office
route-map firewall->inside permit 1
match ip address prefix-list fabric
route-map firewall->inside permit 2
match ipv6 address prefix-list fabric
route-map firewall->inside permit 10
match ip address prefix-list default
route-map firewall->inside permit 11
match ipv6 address prefix-list default
# Outside ↔ firewall.
route-map outside->firewall permit 10
match ip address prefix-list default
route-map outside->firewall permit 11
match ipv6 address prefix-list default
route-map firewall->outside permit 1
match ip address prefix-list fabric
route-map firewall->outside permit 2
match ipv6 address prefix-list fabric
route-map firewall->outside permit 21
match ipv6 address prefix-list office
route-map firewall->outside permit 30
match ip address prefix-list nat
# Tag routes from each firewall. Set weight for primary to 200 and secondary to 100.
{% for firewall in ifaces_firewall %}
route-map firewall-{{ loop.index }}->inside permit 1
set tag {{ loop.index }}
set weight {{ 100 * loop.index }}
call firewall->inside
route-map firewall-{{ loop.index }}->outside permit 1
set tag {{ loop.index }}
set weight {{ 100 * loop.index }}
call firewall->outside
{% endfor %}
# Backup routes over peer link are announced to the peer with BGP
# metrics 190 and 90. These values are copied to weights by receiving
# peer, to be used alongside local routes with weights 200 and 100.
# These are the route maps for peerlink in the inside VRF.
{% for firewall in ifaces_firewall %}
{% set metric = 100 * loop.index - 10 %}
route-map me->peer.2 permit {{ loop.index }}
match tag {{ loop.index }}
on-match goto 100
set metric {{ metric }}
route-map peer.2->me permit {{ loop.index }}
match metric {{ metric }}
on-match goto 100
set weight {{ metric }}
{% endfor %}
# Advertised backup routes for paths that go through the firewall
# (default route).
route-map me->peer.2 permit 110
match ip address prefix-list default
route-map me->peer.2 permit 111
match ipv6 address prefix-list default
# Received backup routes (same as above).
route-map peer.2->me permit 110
match ip address prefix-list default
route-map peer.2->me permit 111
match ipv6 address prefix-list default
# These are the route maps for peerlink in the outside VRF.
{% for firewall in ifaces_firewall %}
{% set metric = 100 * loop.index - 10 %}
route-map me->peer.4 permit {{ loop.index }}
match tag {{ loop.index }}
on-match goto 100
set metric {{ metric }}
route-map peer.4->me permit {{ loop.index }}
match metric {{ metric }}
on-match goto 100
set weight {{ metric }}
{% endfor %}
# Backup routes for uplink and paths that go through the firewall
# (default route and NAT/IPv6 addresses for office networks).
route-map me->peer.4 permit 110
match ip address prefix-list default
route-map me->peer.4 permit 111
match ipv6 address prefix-list default
route-map me->peer.4 permit 120
match ip address prefix-list nat
route-map me->peer.4 permit 131
match ipv6 address prefix-list office
# Received backup routes (same as above).
route-map peer.4->me permit 110
match ip address prefix-list default
route-map peer.4->me permit 111
match ipv6 address prefix-list default
route-map peer.4->me permit 120
match ip address prefix-list nat
route-map peer.4->me permit 131
match ipv6 address prefix-list office

View file

@ -0,0 +1,16 @@
{% set bridge = interfaces | selectattr('type') | selectattr('type.value', '==', 'bridge') | first %}
{% set dhcp_networks = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='role=dhcp-pool', raw_data=true)
| selectattr('vlan') | map(attribute='vlan.vid') | sort -%}
# What servers should the DHCP relay forward requests to?
SERVERS="{{ dhcp }}"
# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests?
# Always include the interface towards the DHCP server.
# This variable requires a -i for each interface configured above.
# This will be used in the actual dhcrelay command
# For example, "-i eth0 -i eth1"
INTF_CMD="{{ bridge.tagged_vlans | map(attribute='vid') | intersect(dhcp_networks) | sort | map('regex_replace', '^', '-id bridge.') | join(' ') }} -iu {{ iface_uplink }} -iu peerlink.4"
# Additional options that are passed to the DHCP relay daemon?
OPTIONS="-U outside"

View file

@ -0,0 +1,22 @@
{% set exits = [inventory_hostname, peer] -%}
global_defs {
enable_script_security
script_user root
}
vrrp_instance dhcrelay {
virtual_router_id 50
virtual_ipaddress { 169.254.1.1/24 }
interface peerlink.4
{% for exit in exits %}
@{{ exit }} priority {{ loop.index }}
@{{ exit }} unicast_src_ip {{ "169.254.1.0/24" | ipaddr(loop.index + 1) | ipaddr('address') }}
{% endfor %}
unicast_peer {
{% for exit in exits %}
@^{{ exit }} {{ "169.254.1.0/24" | ipaddr(loop.index + 1) | ipaddr('address') }}
{% endfor %}
}
notify /usr/local/bin/keepalive-service
}

View file

@ -0,0 +1,35 @@
{# Note that there must be exactly one VLAN-aware bridge. #}
{% set bridge = interfaces | selectattr('type') | selectattr('type.value', '==', 'bridge') | first %}
{% set my_vlans = bridge.tagged_vlans | sort(attribute='vid') -%}
# VRFs.
{% for vlan in my_vlans %}
auto {{ vlan.name }}
iface {{ vlan.name }}
vrf-table auto
{% endfor %}
# Interfaces.
{% for vlan in my_vlans %}
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true)
| map(attribute='prefix') %}
auto {{ bridge.name }}.{{ vlan.vid }}
iface {{ bridge.name }}.{{ vlan.vid }}
vrf {{ vlan.name }}
mtu 9216
{% if peer is defined %}
{% set my_index = inventory_hostname.split('-')[1]|int %}
{% for prefix in prefixes %}
address {{ prefix | ipaddr(1 + my_index) }}
{% endfor %}
{% if prefixes %}
address-virtual 00:00:5e:00:01:01 {{ prefixes | ipaddr(1) | join(' ') }}
{% endif %}
{% else %}
{% for prefix in prefixes %}
address {{ prefix }}
{% endfor %}
{% endif %}
{% endfor %}

View file

@ -0,0 +1,16 @@
{# Note that there must be exactly one VLAN-aware bridge. #}
{% set bridge = interfaces | selectattr('type') | selectattr('type.value', '==', 'bridge') | first %}
{% set my_vlans = bridge.tagged_vlans | sort(attribute='vid') -%}
# Send IPv6 RAs from virtual router IP for each network. Also set DNS options.
# Both exits announce the same gateway, so don’t revoke it if we go down.
{% for vlan in my_vlans %}
interface bridge-{{ vlan.vid }}-v0 {
AdvSendAdvert on;
RemoveAdvOnExit off;
prefix ::/64;
RDNSS {{ dns6 | join(' ') }} { };
DNSSL {{ domain }} { };
};
{% endfor %}