Compare commits

...

10 commits

Author SHA1 Message Date
Timotej Lazar 668af8bdb6 firewall: use a handler to reboot 2024-05-19 10:10:02 +02:00
Timotej Lazar 0e9dac6985 fabric: support arbitrary port breakouts
Not that we use anything but 1x and 4x. Mainly done so I can drop
nonexistent (because they have been broken out) interfaces from NetBox.
2024-05-13 17:44:39 +02:00
Timotej Lazar 16f34c4502 Don’t gather facts when setting them 2024-05-13 17:39:47 +02:00
Timotej Lazar 8c82af23e4 firewall: also configure VPN forwards in the app
There we can define forwards only for networks with actual VPN users.
2024-05-03 11:27:27 +02:00
Timotej Lazar 7656c05b2d Revert "firewall: configure NAT from NetBox data"
Changed my mind. All NAT and VPN is configured from the app now.
2024-04-30 20:59:49 +02:00
Timotej Lazar 8a9d47f176 firewall: configure NAT from NetBox data
This is dynamic NAT for (mostly) physical networks. NAT for custom
prefixes can still be defined in the app.
2024-04-28 15:54:01 +02:00
Timotej Lazar 457ab7d3b7 Query prefixes once for all hosts
And group them into vrf_prefixes for VLAN networks and bgp_prefixes for
servers plugged directly into fabric.

This should reduce the number of queries to NetBox when configuring
firewalls and exit switches. Not sure but I think set_fact helps to
avoid queries (as opposed to setting group_vars).
2024-04-28 12:14:05 +02:00
Timotej Lazar 1c0709a6a6 fabric: allow all VLANs on bridge
Don’t try to guess what should be allowed because not all switch links
are tagged in NetBox. For now we limit mainly on access switches.
2024-04-27 11:30:20 +02:00
Timotej Lazar c07c03a430 Set default inventory 2024-04-27 11:04:02 +02:00
Timotej Lazar 2443a90bc5 fabric: use FHRP groups for virtual router IPs
More realistic- and supported-like and also avoids duplicated gateway
addresses.
2024-04-14 15:15:48 +02:00
17 changed files with 75 additions and 88 deletions

View file

@ -24,11 +24,11 @@ Create a read-only token in NetBox. Set variables required to access NetBox:
Run one-off tasks with (add `--key-file` or other options as necessary): Run one-off tasks with (add `--key-file` or other options as necessary):
ansible -i inventory.yml -m ping 'spine-*' ansible -m ping 'spine-*'
Run a playbook with: Run a playbook with:
ansible-playbook setup.yml -i inventory.yml -l 'spine-*' ansible-playbook setup.yml -l 'spine-*'
## NetBox data ## NetBox data

View file

@ -1,5 +1,6 @@
[defaults] [defaults]
nocows = true nocows = true
filter_plugins = filter_plugins
inventory = inventory.yml
remote_user = root remote_user = root
vault_identity = network vault_identity = network
filter_plugins = filter_plugins

View file

@ -1 +0,0 @@
vlans: "{{ query('netbox.netbox.nb_lookup', 'vlans', api_filter='group=new-net', raw_data=true) | sort(attribute='vid') }}"

View file

@ -213,17 +213,15 @@ ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32 ip prefix-list fabric permit 10.34.0.0/24 ge 32
ipv6 prefix-list fabric permit 2001:1470:fffd:3400::/64 ge 128 ipv6 prefix-list fabric permit 2001:1470:fffd:3400::/64 ge 128
{% for vrf in inside_vrfs %} {% for prefix in vrf_prefixes
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vrf_id='~vrf.id, raw_data=true) | selectattr('vrf.id', 'in', inside_vrfs|map(attribute='id'))
| sort(attribute='family.value') %} | sort(attribute='family.value') | sort(attribute='vlan.vid') %}
{% for prefix in prefixes %}
{% if prefix.family.value == 4 %} {% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }} ge 24 ip prefix-list office permit {{ prefix.prefix }} ge 24
{% else %} {% else %}
ipv6 prefix-list office permit {{ prefix.prefix }} ge 64 ipv6 prefix-list office permit {{ prefix.prefix }} ge 64
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %}
{% if wg_net is defined %} {% if wg_net is defined %}
ip prefix-list vpn permit {{ wg_net | ipaddr('subnet') }} ip prefix-list vpn permit {{ wg_net | ipaddr('subnet') }}
@ -237,7 +235,7 @@ ip prefix-list nat permit {{ wg_ip | ipaddr('host') }}
ip prefix-list nat permit {{ network }} ip prefix-list nat permit {{ network }}
{% endfor %} {% endfor %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true, api_filter='role=bgp') | selectattr('tenant') %} {% for prefix in bgp_prefixes | sort(attribute='family.value') %}
{% if prefix.family.value == 4 %} {% if prefix.family.value == 4 %}
ip prefix-list dc permit {{ prefix.prefix }} ge 32 ip prefix-list dc permit {{ prefix.prefix }} ge 32
{% else %} {% else %}
@ -281,7 +279,7 @@ route-map inside-import permit 21
match ipv6 address prefix-list office match ipv6 address prefix-list office
# Route maps for advertised and received routes. # Route maps for advertised and received routes.
# Inside ↔ fabric. # Default VRF ↔ fabric.
route-map default->fabric permit 10 route-map default->fabric permit 10
match ip address prefix-list default match ip address prefix-list default
route-map default->fabric permit 11 route-map default->fabric permit 11
@ -296,7 +294,7 @@ route-map fabric->default permit 20
route-map fabric->default permit 21 route-map fabric->default permit 21
match ipv6 address prefix-list dc match ipv6 address prefix-list dc
# Inside ↔ firewall. # Inside VRF ↔ firewall.
route-map inside->firewall permit 1 route-map inside->firewall permit 1
match interface lo match interface lo
route-map inside->firewall permit 20 route-map inside->firewall permit 20
@ -313,7 +311,7 @@ route-map firewall->inside permit 10
route-map firewall->inside permit 11 route-map firewall->inside permit 11
match ipv6 address prefix-list default match ipv6 address prefix-list default
# Outside ↔ firewall. # Outside VRF ↔ firewall.
route-map outside->firewall permit 10 route-map outside->firewall permit 10
match ip address prefix-list default match ip address prefix-list default
route-map outside->firewall permit 11 route-map outside->firewall permit 11

View file

@ -1,5 +1,5 @@
{% set dhcp_networks = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='role=dhcp-pool', raw_data=true) {% set dhcp_vlans = vrf_prefixes | selectattr('custom_fields.dhcp_ranges')
| selectattr('vlan') | map(attribute='vlan.vid') | sort -%} | map(attribute='vlan.vid') | sort -%}
# What servers should the DHCP relay forward requests to? # What servers should the DHCP relay forward requests to?
SERVERS="{{ dhcp }}" SERVERS="{{ dhcp }}"
@ -10,7 +10,7 @@ SERVERS="{{ dhcp }}"
# This will be used in the actual dhcrelay command # This will be used in the actual dhcrelay command
# For example, "-i eth0 -i eth1" # For example, "-i eth0 -i eth1"
INTF_CMD="{{ interfaces | selectattr('parent') | selectattr('parent.name', '==', 'bridge') INTF_CMD="{{ interfaces | selectattr('parent') | selectattr('parent.name', '==', 'bridge')
| selectattr('untagged_vlan') | selectattr('untagged_vlan.vid', 'in', dhcp_networks) | selectattr('untagged_vlan') | selectattr('untagged_vlan.vid', 'in', dhcp_vlans)
| map(attribute='name') | sort | map('regex_replace', '^', '-id ') | join(' ') }} -iu {{ iface_uplink }} -iu peerlink.4" | map(attribute='name') | sort | map('regex_replace', '^', '-id ') | join(' ') }} -iu {{ iface_uplink }} -iu peerlink.4"
# Additional options that are passed to the DHCP relay daemon? # Additional options that are passed to the DHCP relay daemon?

View file

@ -3,7 +3,7 @@
{# interfaces that belong to this bridge #} {# interfaces that belong to this bridge #}
{% set ports = interfaces | selectattr('enabled') | selectattr('bridge') | selectattr('bridge.name', '==', bridge.name) %} {% set ports = interfaces | selectattr('enabled') | selectattr('bridge') | selectattr('bridge.name', '==', bridge.name) %}
{# allowed VLANs can be specified on the bridge, any of its ports, or all VLANs in NetBox #} {# allowed VLANs can be specified on the bridge, any of its ports, or all VLANs in NetBox #}
{% set my_vlans = bridge.tagged_vlans or (ports | iface_vlans | flatten | sort(attribute='vid') | unique) or vlans %} {% set my_vlans = bridge.tagged_vlans or vlans %}
{% set my_vlan_ids = my_vlans | map(attribute='vid') | sort -%} {% set my_vlan_ids = my_vlans | map(attribute='vid') | sort -%}
auto {{ bridge.name }} auto {{ bridge.name }}

View file

@ -1,11 +1,7 @@
# https://docs.nvidia.com/networking-ethernet-software/cumulus-linux/Layer-1-and-Switch-Ports/Interface-Configuration-and-Management/Switch-Port-Attributes/#breakout-ports # https://docs.nvidia.com/networking-ethernet-software/cumulus-linux/Layer-1-and-Switch-Ports/Interface-Configuration-and-Management/Switch-Port-Attributes/#breakout-ports
{% for interface in interfaces | selectattr('name', 'match', '^swp[0-9]+$') %} {% for interface in interfaces | selectattr('name', 'match', '^swp[0-9]+(s0)?$') %}
{{ interface.name|regex_replace('^swp', '') }}= {# get '1' from 'swp1' and '2' from 'swp2s0' #}
{%- if interfaces|selectattr('name', 'match', '^'+interface.name+'s[0-9]+$') %} {% set port = interface.name | regex_replace('^swp([0-9]+).*$', '\\1') %}
4x {% set count = interfaces | selectattr('name', 'match', '^swp'+port+'(s[0-9])*$') | length %}
{% elif not interface.enabled %} {{ port }}={% if interface.enabled or count > 1 %}{{ count }}x{% else %}disabled{% endif +%}
disabled
{% else %}
1x
{% endif %}
{% endfor %} {% endfor %}

View file

@ -1,3 +1,6 @@
{% set fhrp_assignments = query('netbox.netbox.nb_lookup', 'fhrp-group-assignments', raw_data=true) %}
{% set fhrp_groups = query('netbox.netbox.nb_lookup', 'fhrp-groups', raw_data=true) -%}
{% for iface in interfaces | rejectattr('name', 'in', ('lo', 'bridge')) | rejectattr('mgmt_only') | selectattr('enabled') %} {% for iface in interfaces | rejectattr('name', 'in', ('lo', 'bridge')) | rejectattr('mgmt_only') | selectattr('enabled') %}
auto {{ iface.name }} auto {{ iface.name }}
iface {{ iface.name }} iface {{ iface.name }}
@ -37,13 +40,13 @@ iface {{ iface.name }}
{% endif %} {% endif %}
{#- Addresses. #} {#- Addresses. #}
{% for addr in iface.ip_addresses | rejectattr('role') %} {% for addr in iface.ip_addresses %}
address {{ addr.address }} address {{ addr.address }}
{% endfor %} {% endfor %}
{% set anycast = iface.ip_addresses | selectattr('role') | selectattr('role.value', '==', 'anycast') {% if iface.count_fhrp_groups > 0 %}
| map(attribute='address') %} {% set fhrp_assignment = fhrp_assignments | selectattr('interface.id', '==', iface.id) | first %}
{% if anycast %} {% set fhrp_group = fhrp_groups | selectattr('id', '==', fhrp_assignment.group.id) | first %}
address-virtual 00:00:5e:00:01:01 {{ anycast | ipaddr(1) | join(' ') }} address-virtual 00:00:5e:00:01:01 {{ fhrp_group.ip_addresses | sort(attribute='family') | map(attribute='address') | join(' ') }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View file

@ -0,0 +1,16 @@
# Make expensive lookups to NetBox once for later reference by any host.
- name: Lookup networks and prefixes
set_fact:
vlans: '{{ query("netbox.netbox.nb_lookup", "vlans", api_filter="group=new-net", raw_data=true)
| sort(attribute="vid") }}'
prefixes: '{{ query("netbox.netbox.nb_lookup", "prefixes", raw_data=true)
| sort(attribute="prefix") | sort(attribute="family.value") }}'
- name: Select VLAN and BGP prefixes
set_fact:
vrf_prefixes: '{{ prefixes | selectattr("vrf")
| selectattr("vlan") | selectattr("vlan.id", "in", vlans|map(attribute="id"))
| sort(attribute="vlan.vid") }}'
bgp_prefixes: '{{ prefixes | selectattr("tenant")
| selectattr("role") | selectattr("role.slug", "==", "bgp")
| sort(attribute="tenant.slug") }}'

View file

@ -6,6 +6,10 @@
command: mkinitfs command: mkinitfs
when: "'handler' not in ansible_skip_tags" when: "'handler' not in ansible_skip_tags"
- name: reboot
reboot:
when: "'handler' not in ansible_skip_tags"
- name: reload frr - name: reload frr
command: /usr/lib/frr/frr-reload.py --reload /etc/frr/frr.conf command: /usr/lib/frr/frr-reload.py --reload /etc/frr/frr.conf
when: "'handler' not in ansible_skip_tags" when: "'handler' not in ansible_skip_tags"

View file

@ -3,23 +3,13 @@
dest: /etc/network/interfaces.d/mgmt.intf dest: /etc/network/interfaces.d/mgmt.intf
src: mgmt.intf.j2 src: mgmt.intf.j2
mode: 0644 mode: 0644
register: task_mgmt_interface notify: reboot
- name: Run SSH in management VRF - name: Run SSH in management VRF
lineinfile: lineinfile:
path: /etc/conf.d/sshd path: /etc/conf.d/sshd
regexp: "#* *vrf="
line: "vrf=\"mgmt\"" line: "vrf=\"mgmt\""
register: task_ssh_vrf notify: reboot
- name: Reboot for new VRF - meta: flush_handlers
reboot:
when: task_mgmt_interface.changed or task_ssh_vrf.changed
register: task_reboot
- name: Reset the connection
meta: reset_connection
- name: Wait for the network device to reload
wait_for_connection:
delay: 10
when: task_reboot.changed

View file

@ -82,16 +82,13 @@ ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32 ip prefix-list fabric permit 10.34.0.0/24 ge 32
{% for vlan in vlans %} {% for prefix in vrf_prefixes | rejectattr('vrf.name', '==', 'outside')
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true) %} | sort(attribute='family.value') %}
{% if prefix.vrf and prefix.vrf.name != 'outside' %}
{% if prefix.family.value == 4 %} {% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }} ge 24 ip prefix-list office permit {{ prefix.prefix }} ge 24
{% elif prefix.family.value == 6 %} {% elif prefix.family.value == 6 %}
ipv6 prefix-list office permit {{ prefix.prefix }} ge 64 ipv6 prefix-list office permit {{ prefix.prefix }} ge 64
{% endif %} {% endif %}
{% endif %}
{% endfor %}
{% endfor %} {% endfor %}
{% if wg_net is defined %} {% if wg_net is defined %}

View file

@ -1,18 +1,9 @@
{% for vlan in vlans %} {% for family, family_prefixes in vrf_prefixes | groupby('family.value') %}
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true) %} {% for vlan, vlan_prefixes in family_prefixes | groupby('vlan.vid') %}
{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map(attribute='prefix') %} set {{ vlan_prefixes[0].vlan.name }}{% if family == 6 %}/6{% endif %} {
{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map(attribute='prefix') %} type ipv{{ family }}_addr; flags interval
set {{ vlan.name }} { elements = { {{ vlan_prefixes | map(attribute='prefix') | join(',') }} }
type ipv4_addr; flags interval
{% if prefixes4 %}
elements = { {{ prefixes4 | join(', ') }} }
{% endif %}
}
set {{ vlan.name }}/6 {
type ipv6_addr; flags interval
{% if prefixes6 %}
elements = { {{ prefixes6 | join(', ') }} }
{% endif %}
} }
{% endfor %}
{% endfor %} {% endfor %}

View file

@ -80,16 +80,6 @@ table inet filter {
ct status dnat accept \ ct status dnat accept \
comment "Forward DNAT traffic for servers and suchlike" comment "Forward DNAT traffic for servers and suchlike"
# Forward IPv4 to/from VPN users in the same network.
{% for vlan in vlans %}
iif @inside ip saddr @{{ vlan.name }} ip daddr @{{ vlan.name }} accept
{% endfor %}
# Forward IPv6 to/from VPN users in the same network.
{% for vlan in vlans %}
iif @inside ip6 saddr @{{ vlan.name }}/6 ip6 daddr @{{ vlan.name }}/6 accept
{% endfor %}
include "/etc/nftables.d/forward.nft*" include "/etc/nftables.d/forward.nft*"
} }

View file

@ -1,11 +1,8 @@
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true) -%}
{ {
{% for vlan in vlans %} {% for vlan, addrs in vrf_prefixes | groupby('vlan.vid') %}
{% set vlan_prefixes = prefixes | selectattr('vlan') | selectattr('vlan.id', '==', vlan.id) | map(attribute='prefix') %} "{{ addrs[0].vlan.name }}": {
"{{ vlan.name }}": { "ip": {{ addrs | selectattr('family.value', '==', 4) | map(attribute='prefix') | to_json }},
"ip": {{ vlan_prefixes | ipv4 | to_json }}, "ip6": {{ addrs | selectattr('family.value', '==', 6) | map(attribute='prefix') | to_json }}
"ip6": {{ vlan_prefixes | ipv6 | to_json }}
}{% if not loop.last %},{% endif +%} }{% if not loop.last %},{% endif +%}
{% endfor %} {% endfor %}
} }

View file

@ -78,16 +78,13 @@ route-map loopbacks permit 10
ip prefix-list default permit 0.0.0.0/0 ip prefix-list default permit 0.0.0.0/0
ipv6 prefix-list default permit ::/0 ipv6 prefix-list default permit ::/0
{% for tenant in my_tenants %} {% for prefix in bgp_prefixes | selectattr('tenant.slug', 'in', my_tenants) %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true, api_filter='tenant='~tenant)
| selectattr('role') | selectattr('role.slug', '==', 'bgp') | rejectattr('vlan') %}
{% if prefix.family.value == 4 %} {% if prefix.family.value == 4 %}
ip prefix-list dc-{{ tenant }} permit {{ prefix.prefix }} ge 32 ip prefix-list dc-{{ prefix.tenant.slug }} permit {{ prefix.prefix }} ge 32
{% else %} {% else %}
ipv6 prefix-list dc-{{ tenant }} permit {{ prefix.prefix }} ge 64 ipv6 prefix-list dc-{{ prefix.tenant.slug }} permit {{ prefix.prefix }} ge 64
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %}
# We only announce the default route to DC servers. # We only announce the default route to DC servers.
route-map default->dc permit 10 route-map default->dc permit 10

View file

@ -1,3 +1,9 @@
- hosts: '*'
gather_facts: false
roles:
- facts
# Set up fabric.
- hosts: spine-* - hosts: spine-*
roles: roles:
- spine - spine
@ -10,11 +16,13 @@
roles: roles:
- exit - exit
# Set up access switches.
- hosts: access-*, sw-* - hosts: access-*, sw-*
gather_facts: false gather_facts: false
roles: roles:
- access - access
# Set up firewall.
- hosts: fw-* - hosts: fw-*
roles: roles:
- firewall - firewall