From e6027e9078b2e0641b79093f48500c6d3c000068 Mon Sep 17 00:00:00 2001 From: Shirkanesi Date: Sun, 15 Sep 2024 09:41:09 +0200 Subject: [PATCH] ADD import roles --- .gitignore | 176 ++++++++++++ roles/bird/defaults/main.yml | 3 + roles/bird/handlers/main.yml | 5 + roles/bird/tasks/main.yml | 62 +++++ roles/bird/templates/bird.conf.j2 | 261 ++++++++++++++++++ roles/bird/templates/sysctl.conf.j2 | 2 + roles/bird/vars/debian.yml | 3 + roles/bird/vars/main.yml | 1 + roles/interfaces/defaults/main.yml | 2 + roles/interfaces/handlers/main.yml | 12 + roles/interfaces/tasks/main.yml | 57 ++++ .../templates/60-persistent-net.rules.j2 | 5 + roles/interfaces/templates/ifupdown2.j2 | 43 +++ roles/interfaces/vars/debian.yml | 5 + roles/interfaces/vars/main.yml | 1 + roles/isc_dhcp/defaults/main.yml | 1 + roles/isc_dhcp/handlers/main.yml | 6 + roles/isc_dhcp/tasks/main.yml | 64 +++++ roles/isc_dhcp/templates/dhcpd.conf.j2 | 21 ++ roles/isc_dhcp/templates/isc-dhcp-server.j2 | 2 + roles/isc_dhcp/vars/debian.yml | 3 + roles/isc_dhcp/vars/main.yml | 1 + roles/keepalived/defaults/main.yml | 2 + roles/keepalived/handlers/main.yml | 9 + roles/keepalived/tasks/main.yml | 52 ++++ roles/keepalived/templates/keepalived.conf.j2 | 27 ++ roles/keepalived/vars/debian.yml | 3 + roles/keepalived/vars/main.yml | 1 + roles/radvd/defaults/main.yml | 1 + roles/radvd/handlers/main.yml | 6 + roles/radvd/tasks/main.yml | 52 ++++ roles/radvd/templates/radvd.conf.j2 | 24 ++ roles/radvd/vars/debian.yml | 3 + roles/radvd/vars/main.yml | 1 + .../filter_plugins/router_filters.py | 193 +++++++++++++ roles/router_bundle/tasks/main.yml | 19 ++ 36 files changed, 1129 insertions(+) create mode 100644 roles/bird/defaults/main.yml create mode 100644 roles/bird/handlers/main.yml create mode 100644 roles/bird/tasks/main.yml create mode 100644 roles/bird/templates/bird.conf.j2 create mode 100644 roles/bird/templates/sysctl.conf.j2 create mode 100644 roles/bird/vars/debian.yml create mode 100644 roles/bird/vars/main.yml create mode 100644 roles/interfaces/defaults/main.yml create mode 100644 roles/interfaces/handlers/main.yml create mode 100644 roles/interfaces/tasks/main.yml create mode 100644 roles/interfaces/templates/60-persistent-net.rules.j2 create mode 100644 roles/interfaces/templates/ifupdown2.j2 create mode 100644 roles/interfaces/vars/debian.yml create mode 100644 roles/interfaces/vars/main.yml create mode 100644 roles/isc_dhcp/defaults/main.yml create mode 100644 roles/isc_dhcp/handlers/main.yml create mode 100644 roles/isc_dhcp/tasks/main.yml create mode 100644 roles/isc_dhcp/templates/dhcpd.conf.j2 create mode 100644 roles/isc_dhcp/templates/isc-dhcp-server.j2 create mode 100644 roles/isc_dhcp/vars/debian.yml create mode 100644 roles/isc_dhcp/vars/main.yml create mode 100644 roles/keepalived/defaults/main.yml create mode 100644 roles/keepalived/handlers/main.yml create mode 100644 roles/keepalived/tasks/main.yml create mode 100644 roles/keepalived/templates/keepalived.conf.j2 create mode 100644 roles/keepalived/vars/debian.yml create mode 100644 roles/keepalived/vars/main.yml create mode 100644 roles/radvd/defaults/main.yml create mode 100644 roles/radvd/handlers/main.yml create mode 100644 roles/radvd/tasks/main.yml create mode 100644 roles/radvd/templates/radvd.conf.j2 create mode 100644 roles/radvd/vars/debian.yml create mode 100644 roles/radvd/vars/main.yml create mode 100644 roles/router_bundle/filter_plugins/router_filters.py create mode 100644 roles/router_bundle/tasks/main.yml diff --git a/.gitignore b/.gitignore index 6bcf0e5..6423cf6 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,179 @@ tags .ionide # End of https://www.toptal.com/developers/gitignore/api/ansible,vim,visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/roles/bird/defaults/main.yml b/roles/bird/defaults/main.yml new file mode 100644 index 0000000..1ae2253 --- /dev/null +++ b/roles/bird/defaults/main.yml @@ -0,0 +1,3 @@ +bird_enabled: false +bird_bgp_advertised_networks: [] +bird_bgp_neighbors: [] diff --git a/roles/bird/handlers/main.yml b/roles/bird/handlers/main.yml new file mode 100644 index 0000000..b7ce9d9 --- /dev/null +++ b/roles/bird/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reconfigure bird + shell: /usr/sbin/birdc configure +- name: Reload sysctl + ansible.builtin.command: sysctl --system diff --git a/roles/bird/tasks/main.yml b/roles/bird/tasks/main.yml new file mode 100644 index 0000000..17085db --- /dev/null +++ b/roles/bird/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: Set OS dependent variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + - "{{ ansible_system | lower }}.yml" + paths: + - "{{ role_path }}/vars" + ignore_errors: True + tags: + - always + +- name: OS is supported + assert: + that: __os_supported + quiet: True + vars: + __os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}" + tags: + - always + +- name: Install required packages + package: + name: "{{ item }}" + state: present + with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}" + tags: + - install + +- name: Enable routing in sysctl + template: + src: sysctl.conf.j2 + dest: /etc/sysctl.d/90-routing.conf + owner: root + group: root + mode: "0644" + notify: Reload sysctl + +- name: Install bird configuration + template: + src: bird.conf.j2 + dest: "/etc/bird/bird.conf" + owner: root + group: root + mode: 0755 + tags: + - configure + no_log: false + when: bird_enabled + notify: Reconfigure bird + +- name: Enable bird service + ansible.builtin.systemd_service: + name: bird.service + state: "{{ 'started' if bird_enabled else 'stopped' }}" + enabled: "{{ bird_enabled }}" + when: not ansible_check_mode diff --git a/roles/bird/templates/bird.conf.j2 b/roles/bird/templates/bird.conf.j2 new file mode 100644 index 0000000..7b3e13e --- /dev/null +++ b/roles/bird/templates/bird.conf.j2 @@ -0,0 +1,261 @@ +# {{ ansible_managed }} + +router id {{ bird_router_id }}; + + +protocol device { + scan time 10; +} + +protocol kernel { + scan time 5; + + ipv6 { + import none; + export filter { +{% if bird_prefsrc is defined %} + krt_prefsrc = {{ bird_prefsrc }}; +{% endif %} + if source = RTS_BGP then accept; + if source = RTS_STATIC then accept; + if source = RTS_DEVICE then reject; + accept; + }; + }; +} + +protocol kernel { + scan time 5; + + ipv4 { + import none; + export filter { +{% if bird_prefsrc_v4 is defined %} + krt_prefsrc = {{ bird_prefsrc_v4 }}; +{% endif %} + if source = RTS_BGP then accept; + if source = RTS_STATIC then accept; + if source = RTS_DEVICE then reject; + accept; + }; + }; +} + +protocol static { +{% for net in bird_bgp_advertised_networks %} + route {{ net }} reject; +{% endfor %} + + ipv6 { + import all; + export none; + }; +} + +protocol static { +{% for net in bird_bgp_advertised_networks_v4 | default([]) %} + route {{ net }} reject; +{% endfor %} + ipv4 { + import all; + export none; + }; +} + +{%if bird_ospf_interfaces | default([]) | length > 0%} +############################################ +# _|_| _|_|_| _|_|_| _|_|_|_| # +# _| _| _| _| _| _| # +# _| _| _|_| _|_|_| _|_|_| # +# _| _| _| _| _| # +# _|_| _|_|_| _| _| # +############################################ + +# Filter to export all direct and bgp routes into OSPF +filter export_all_OSPF { + if ( source = RTS_DEVICE ) then accept; + if ( source = RTS_STATIC_DEVICE ) then accept; + if ( source = RTS_STATIC ) then accept; + if ( source = RTS_BGP ) then accept; + reject; +} + +protocol ospf v3 ospf6 { + ipv6 { +{% if bird_ospf_export_all | default(false) %} + export filter export_all_OSPF; +{% endif %} + }; + area 0 { +{%for iface in bird_ospf_interfaces%} + interface "{{ iface.name }}" { +{%if iface.stub | default(false)%} + stub; +{%endif%} + }; +{%endfor%} + }; +} +{%endif%} + + +{% if bird_bgp_neighbors | default([]) | length > 0%} +################################### +# _|_|_| _|_|_| _|_|_| # +# _| _| _| _| _| # +# _|_|_| _| _|_| _|_|_| # +# _| _| _| _| _| # +# _|_|_| _|_|_| _| # +################################### +define OWNNETSET = [{{ bird_bgp_advertised_networks | join(', ') }}]; +define OWNNETSET_v4 = [{{ bird_bgp_advertised_networks_v4 | default([]) | join(', ') }}]; +define OWNASN = {{ bird_bgp_local_asn }}; + +function is_self_net() { + if net ~ OWNNETSET then return true; + if net ~ OWNNETSET_v4 then return true; + return false; +} + +function has_private_asn() { + # Check if the AS path contains any private ASN + if (bgp_path ~ [64512..65534]) || (bgp_path ~ [4200000000..4294967294]) then { + print "Rejecting route with private ASN in AS path: ", bgp_path; + return true; + } + return false; +} + +function is_reserved_net() { + # ULA + if net ~ fc00::/7 then return true; + # Link-local + if net ~ fe80::/10 then return true; + # Documentation + if net ~ 2001:db8::/32 then return true; + # Discard + if net ~ 100::/64 then return true; + # Orchid + if net ~ 2001:20::/28 then return true; + + # IPv4 + if net ~ 0.0.0.0/8 then return true; + if net ~ 10.0.0.0/8 then return true; + if net ~ 100.64.0.0/10 then return true; + if net ~ 127.0.0.0/8 then return true; + if net ~ 169.254.0.0/16 then return true; + if net ~ 172.16.0.0/12 then return true; + if net ~ 192.0.0.0/24 then return true; + if net ~ 192.0.2.0/24 then return true; + if net ~ 192.88.99.0/24 then return true; + if net ~ 192.168.0.0/16 then return true; + if net ~ 198.18.0.0/15 then return true; + if net ~ 198.51.100.0/24 then return true; + if net ~ 203.0.113.0/24 then return true; + if net ~ 224.0.0.0/4 then return true; + if net ~ 233.252.0.0/24 then return true; + if net ~ 240.0.0.0/4 then return true; + if net ~ 255.255.255.255/32 then return true; + return false; +} + +{%for peer in bird_bgp_neighbors%} +# {{ peer.name }} +{% set peer_name = peer.name | lower | regex_replace('[\s\.\-\$%&/()=]', '_') %} + +filter {{ peer_name }}_BGP_IMPORT { + # don't re-import networks announced by ourself + if is_self_net() then reject; +{% if not (peer.allow_reserved_networks | default(bird_bgp_allow_reserved_networks | default(false))) %} + # don't import reserved networks + if is_reserved_net() then reject; +{% endif %} + # never accept routes more specific than /{{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} + if net.len > {{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} then reject; +{% if not (peer.allow_private_asn | default(bird_bgp_allow_private_asn | default(false))) %} + # prevent importing paths with private ASNs + if has_private_asn() then reject; +{% endif %} +{% if (peer.max_as_path_length is defined) or (bird_bgp_max_as_path_length is defined) %} + if bgp_path.len > {{ peer.max_as_path_length | default(bird_bgp_max_as_path_length) }} then reject; +{% endif %} +{% if (peer.restrict_prefixes | default([]) | length > 0) %} + if net !~ [{{ peer.restrict_prefixes | join(', ') }}] then reject; +{% endif %} + accept; +} + +# export rules for {{ peer_name }} +filter {{ peer_name }}_ALLOWED_BGP { +{% for n in range(peer.as_prepend | default(1)) %} + bgp_path.prepend({{ peer.local_asn | default('OWNASN') }}); +{% endfor %} +{% if not (peer.allow_reserved_networks | default(bird_bgp_allow_reserved_networks | default(false))) %} + # don't export reserved networks + if is_reserved_net() then reject; +{% endif %} + # never send routes more specific than /{{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} + if net.len > {{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} then reject; +{% if not (peer.allow_private_asn | default(bird_bgp_allow_private_asn | default(false))) %} + # prevent exposing paths with private ASNs + if has_private_asn() then reject; +{% endif %} + if is_self_net() then accept; +{% if (peer.redistribute_learned | default(false)) %} + # redistribute all other routes learned from BGP + if source = RTS_BGP then { +{% if not (peer.redistribute_reserved | default(false)) %} + # don't redistribute reserved networks + if is_reserved_net() then reject; + +{% endif %} +{% for n in range(peer.redistribute_prepend | default(0)) %} + bgp_path.prepend({{ peer.local_asn | default('OWNASN') }}); +{% endfor %} + accept; + } +{% endif %} + reject; +} +protocol bgp {{ peer_name }} { + local {{ peer.local_ip }} as {{ peer.local_asn | default('OWNASN') }}; + neighbor {{ peer.neighbor_ip }} as {{ peer.neighbor_asn }}; + description "{{ peer.description | default(peer.name) }}"; +{% if peer.password is defined %} + password "{{ peer.password }}"; +{% endif %} +{% if peer.metric is defined %} + path metric {{ peer.metric }}; +{% endif %} + +{% if peer.ipv4 | default(false) == true %} + ipv4 { + import filter {{ peer_name }}_BGP_IMPORT; + export filter {{ peer_name }}_ALLOWED_BGP; +{% if peer.import_limit | default(1000) != false %} + import limit {{ peer.import_limit | default(1000) }} action block; +{% endif %} + next hop {{ ('address ' + peer.next_hop) if peer.next_hop is defined else 'self' }}; + }; +{% else %} + ipv6 { + import filter {{ peer_name }}_BGP_IMPORT; + export filter {{ peer_name }}_ALLOWED_BGP; +{% if peer.import_limit | default(1000) != false %} + import limit {{ peer.import_limit | default(1000) }} action block; +{% endif %} + next hop {{ ('address ' + peer.next_hop) if peer.next_hop is defined else 'self' }}; + }; +{% endif %} +} +{%endfor%} + + +# TODO: roa with routinator... + +# roa4 table dn42_roa; +# protocol static { +# roa4 { table dn42_roa; }; +# include "/etc/bird/roa_dn42.conf"; +# }; +{% endif %} diff --git a/roles/bird/templates/sysctl.conf.j2 b/roles/bird/templates/sysctl.conf.j2 new file mode 100644 index 0000000..f501eb9 --- /dev/null +++ b/roles/bird/templates/sysctl.conf.j2 @@ -0,0 +1,2 @@ +net.ipv4.ip_forward = 1 +net.ipv6.conf.all.forwarding = 1 diff --git a/roles/bird/vars/debian.yml b/roles/bird/vars/debian.yml new file mode 100644 index 0000000..cbc93cc --- /dev/null +++ b/roles/bird/vars/debian.yml @@ -0,0 +1,3 @@ +bird_os_supported: true +bird_packages: + - bird2 diff --git a/roles/bird/vars/main.yml b/roles/bird/vars/main.yml new file mode 100644 index 0000000..9b2b6f2 --- /dev/null +++ b/roles/bird/vars/main.yml @@ -0,0 +1 @@ +bird_os_supported: false diff --git a/roles/interfaces/defaults/main.yml b/roles/interfaces/defaults/main.yml new file mode 100644 index 0000000..462da0e --- /dev/null +++ b/roles/interfaces/defaults/main.yml @@ -0,0 +1,2 @@ +net_interfaces: [] +interfaces_manage_ifupdown: true diff --git a/roles/interfaces/handlers/main.yml b/roles/interfaces/handlers/main.yml new file mode 100644 index 0000000..2dd10fa --- /dev/null +++ b/roles/interfaces/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: Reload udev + # TODO! FIXME! + systemd_service: + name: systemd-udev-trigger.service + state: restarted +- name: Reload ifupdown + # TODO! FIXME! This drops some packages. Can this be done better? + systemd_service: + name: networking.service + state: restarted + throttle: 1 diff --git a/roles/interfaces/tasks/main.yml b/roles/interfaces/tasks/main.yml new file mode 100644 index 0000000..b0eafc9 --- /dev/null +++ b/roles/interfaces/tasks/main.yml @@ -0,0 +1,57 @@ +--- +- name: Set OS dependent variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + - "{{ ansible_system | lower }}.yml" + paths: + - "{{ role_path }}/vars" + ignore_errors: True + tags: + - always + +- name: OS is supported + assert: + that: __os_supported + quiet: True + vars: + __os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}" + tags: + - always + +- name: Install required packages + package: + name: "{{ item }}" + state: present + with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}" + tags: + - install + +- name: Install persistent if-names + template: + src: 60-persistent-net.rules.j2 + dest: "/etc/udev/rules.d/60-persistent-net.rules" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Reload udev + +- name: Install ifupdown config + template: + src: ifupdown2.j2 + dest: "/etc/network/interfaces.d/ansible" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Reload ifupdown diff --git a/roles/interfaces/templates/60-persistent-net.rules.j2 b/roles/interfaces/templates/60-persistent-net.rules.j2 new file mode 100644 index 0000000..39da4c7 --- /dev/null +++ b/roles/interfaces/templates/60-persistent-net.rules.j2 @@ -0,0 +1,5 @@ +{% for iface in net_interfaces %} +{% if iface.name is defined and iface.mac is defined %} +SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="{{ iface.mac }}", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="e*", NAME="{{ iface.name }}" +{% endif %} +{% endfor %} diff --git a/roles/interfaces/templates/ifupdown2.j2 b/roles/interfaces/templates/ifupdown2.j2 new file mode 100644 index 0000000..f2e970f --- /dev/null +++ b/roles/interfaces/templates/ifupdown2.j2 @@ -0,0 +1,43 @@ +# {{ ansible_managed }} + +{% macro iface_config(iface, type, inet) %} +iface {{ iface.name }} {{ type }} {{ inet.conf | default('manual') }} +{% if iface.vlan_raw_device is defined %} + vlan-raw-device {{ iface.vlan_raw_device }} +{% endif %} +{% if iface.dummy | default(false) %} + pre-up ip link show $IFACE > /dev/null || sudo ip link add $IFACE type dummy + post-down ip link show $IFACE > /dev/null && sudo ip link del $IFACE +{% endif %} +{% for addr in inet.addr %} + up ip addr add {{ addr }} dev $IFACE + pre-down ip addr del {{ addr }} dev $IFACE +{% endfor %} +{% if inet.gateway is defined and inet.gateway %} + up ip r add default via {{ inet.gateway }} dev $IFACE + pre-down ip r del default via {{ inet.gateway }} dev $IFACE +{% endif %} +{% if inet.routes is defined and inet.routes %} +{% for route in inet.routes %} + post-up ip r add {{ route.dst }} via {{ route.via }} dev $IFACE + pre-down ip r del {{ route.dst }} via {{ route.via }} dev $IFACE +{% endfor %} +{% endif %} +{% if inet.custom is defined and inet.custom %} + {{ inet.custom | default('') | indent(4) }} +{% endif %} +{% endmacro %} + +{% for iface in net_interfaces %} +{% if interfaces_manage_ifupdown or (iface.ifupdown | default(false)) %} +# {{ iface.name }} +allow-hotplug {{ iface.name }} +{% if iface.inet is defined and iface.inet %} +{{ iface_config(iface, 'inet', iface.inet) }} +{% endif %} +{% if iface.inet6 is defined and iface.inet6 %} +{{ iface_config(iface, 'inet6', iface.inet6) }} +{% endif %} + +{% endif %} +{% endfor %} diff --git a/roles/interfaces/vars/debian.yml b/roles/interfaces/vars/debian.yml new file mode 100644 index 0000000..efbbde1 --- /dev/null +++ b/roles/interfaces/vars/debian.yml @@ -0,0 +1,5 @@ +interfaces_os_supported: true +interfaces_packages: + - ifupdown + - udev + - vlan diff --git a/roles/interfaces/vars/main.yml b/roles/interfaces/vars/main.yml new file mode 100644 index 0000000..202148f --- /dev/null +++ b/roles/interfaces/vars/main.yml @@ -0,0 +1 @@ +interfaces_os_supported: false diff --git a/roles/isc_dhcp/defaults/main.yml b/roles/isc_dhcp/defaults/main.yml new file mode 100644 index 0000000..9fda095 --- /dev/null +++ b/roles/isc_dhcp/defaults/main.yml @@ -0,0 +1 @@ +isc_dhcp_enabled: false diff --git a/roles/isc_dhcp/handlers/main.yml b/roles/isc_dhcp/handlers/main.yml new file mode 100644 index 0000000..80f5169 --- /dev/null +++ b/roles/isc_dhcp/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart isc_dhcp + ansible.builtin.systemd_service: + name: isc-dhcp-server.service + state: restarted + enabled: true diff --git a/roles/isc_dhcp/tasks/main.yml b/roles/isc_dhcp/tasks/main.yml new file mode 100644 index 0000000..771c28c --- /dev/null +++ b/roles/isc_dhcp/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: Set OS dependent variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + - "{{ ansible_system | lower }}.yml" + paths: + - "{{ role_path }}/vars" + ignore_errors: True + tags: + - always + +- name: OS is supported + assert: + that: __os_supported + quiet: True + vars: + __os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}" + tags: + - always + +- name: Install required packages + package: + name: "{{ item }}" + state: present + with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}" + when: radvd_enabled + tags: + - install + +- name: Install dhcpd configuration + template: + src: dhcpd.conf.j2 + dest: "/etc/dhcp/dhcpd.conf" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Restart isc_dhcp + +- name: Install dhcpd configuration + template: + src: isc-dhcp-server.j2 + dest: "/etc/default/isc-dhcp-server" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Restart isc_dhcp + +- name: Enable dhcpd service + ansible.builtin.systemd_service: + name: isc-dhcp-server.service + state: started + enabled: true diff --git a/roles/isc_dhcp/templates/dhcpd.conf.j2 b/roles/isc_dhcp/templates/dhcpd.conf.j2 new file mode 100644 index 0000000..9e58912 --- /dev/null +++ b/roles/isc_dhcp/templates/dhcpd.conf.j2 @@ -0,0 +1,21 @@ +# {{ ansible_managed }} + +ddns-update-style none; +authoritative; + +{% for net in isc_dhcp_networks | default([]) %} +# {{ net.interface }} +subnet {{ net.subnet }} netmask {{ net.netmask }} { + interface {{ net.interface }}; + option routers {{ net.router }}; + option subnet-mask {{ net.netmask }}; +{% if net.dns is defined %} + option domain-name-servers {{ net.dns }}; +{% endif %} + option domain-name "{{ net.domain | default('hiwi.net.scc.kit.edu') }}"; + range {{ net.range_start }} {{ net.range_end }}; + default-lease-time 600; + max-lease-time 7200; +} + +{% endfor %} diff --git a/roles/isc_dhcp/templates/isc-dhcp-server.j2 b/roles/isc_dhcp/templates/isc-dhcp-server.j2 new file mode 100644 index 0000000..0c5c194 --- /dev/null +++ b/roles/isc_dhcp/templates/isc-dhcp-server.j2 @@ -0,0 +1,2 @@ +# {{ ansible_managed }} +INTERFACESv4="{{ isc_dhcp_networks | map(attribute='interface') | join(' ') }}" diff --git a/roles/isc_dhcp/vars/debian.yml b/roles/isc_dhcp/vars/debian.yml new file mode 100644 index 0000000..d08a067 --- /dev/null +++ b/roles/isc_dhcp/vars/debian.yml @@ -0,0 +1,3 @@ +isc_dhcp_os_supported: true +isc_dhcp_packages: + - isc-dhcp-server diff --git a/roles/isc_dhcp/vars/main.yml b/roles/isc_dhcp/vars/main.yml new file mode 100644 index 0000000..0e478f2 --- /dev/null +++ b/roles/isc_dhcp/vars/main.yml @@ -0,0 +1 @@ +isc_dhcp_os_supported: false diff --git a/roles/keepalived/defaults/main.yml b/roles/keepalived/defaults/main.yml new file mode 100644 index 0000000..0cffa10 --- /dev/null +++ b/roles/keepalived/defaults/main.yml @@ -0,0 +1,2 @@ +keepalived_enabled: false +keepalived_interfaces: [] diff --git a/roles/keepalived/handlers/main.yml b/roles/keepalived/handlers/main.yml new file mode 100644 index 0000000..a192ae2 --- /dev/null +++ b/roles/keepalived/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Restart keepalived + systemd_service: + name: keepalived.service + state: restarted + enabled: true + when: not ansible_check_mode +- name: Reload sysctl + ansible.builtin.command: sysctl --system diff --git a/roles/keepalived/tasks/main.yml b/roles/keepalived/tasks/main.yml new file mode 100644 index 0000000..28a03a9 --- /dev/null +++ b/roles/keepalived/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Set OS dependent variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + - "{{ ansible_system | lower }}.yml" + paths: + - "{{ role_path }}/vars" + ignore_errors: True + tags: + - always + +- name: OS is supported + assert: + that: __os_supported + quiet: True + vars: + __os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}" + tags: + - always + +- name: Install required packages + package: + name: "{{ item }}" + state: present + with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}" + tags: + - install + +- name: Install keepalived configuration + template: + src: keepalived.conf.j2 + dest: "/etc/keepalived/keepalived.conf" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Restart keepalived + +- name: Enable keepalived service + ansible.builtin.systemd_service: + name: keepalived.service + state: "{{ 'started' if keepalived_enabled else 'stopped' }}" + enabled: "{{ keepalived_enabled }}" + when: not ansible_check_mode diff --git a/roles/keepalived/templates/keepalived.conf.j2 b/roles/keepalived/templates/keepalived.conf.j2 new file mode 100644 index 0000000..655f190 --- /dev/null +++ b/roles/keepalived/templates/keepalived.conf.j2 @@ -0,0 +1,27 @@ +# {{ ansible_managed }} +vrrp_sync_group G1 { + group { +{%for iface in keepalived_interfaces %} + {{ iface.name }} +{% endfor %} + } +} + +{%for iface in keepalived_interfaces %} +vrrp_instance {{ iface.name }} { + state {{ keepalived_init_state | default('BACKUP') }} + interface {{ iface.interface | default(iface.name) }} + virtual_router_id 42 + priority {{ keepalived_host_prio }} + advert_int 2 + authentication { + auth_type PASS + auth_pass {{ iface.password | default(keepalived_password) }} + } + virtual_ipaddress { +{% for addr in iface.addresses %} + {{ addr }} +{% endfor %} + } +} +{% endfor %} diff --git a/roles/keepalived/vars/debian.yml b/roles/keepalived/vars/debian.yml new file mode 100644 index 0000000..bc2ba7d --- /dev/null +++ b/roles/keepalived/vars/debian.yml @@ -0,0 +1,3 @@ +keepalived_os_supported: true +keepalived_packages: + - keepalived diff --git a/roles/keepalived/vars/main.yml b/roles/keepalived/vars/main.yml new file mode 100644 index 0000000..65afec2 --- /dev/null +++ b/roles/keepalived/vars/main.yml @@ -0,0 +1 @@ +keepalived_os_supported: false diff --git a/roles/radvd/defaults/main.yml b/roles/radvd/defaults/main.yml new file mode 100644 index 0000000..14d5e70 --- /dev/null +++ b/roles/radvd/defaults/main.yml @@ -0,0 +1 @@ +radvd_enabled: false diff --git a/roles/radvd/handlers/main.yml b/roles/radvd/handlers/main.yml new file mode 100644 index 0000000..613f31e --- /dev/null +++ b/roles/radvd/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart radvd + ansible.builtin.systemd_service: + name: radvd.service + state: restarted + enabled: true diff --git a/roles/radvd/tasks/main.yml b/roles/radvd/tasks/main.yml new file mode 100644 index 0000000..e80981a --- /dev/null +++ b/roles/radvd/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: Set OS dependent variables + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + - "{{ ansible_system | lower }}.yml" + paths: + - "{{ role_path }}/vars" + ignore_errors: True + tags: + - always + +- name: OS is supported + assert: + that: __os_supported + quiet: True + vars: + __os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}" + tags: + - always + +- name: Install required packages + package: + name: "{{ item }}" + state: present + with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}" + when: radvd_enabled + tags: + - install + +- name: Install radvd configuration + template: + src: radvd.conf.j2 + dest: "/etc/radvd.conf" + owner: root + group: root + mode: 0644 + tags: + - configure + no_log: false + notify: Restart radvd + +- name: Enable radvd service + ansible.builtin.systemd_service: + name: radvd.service + state: started + enabled: true diff --git a/roles/radvd/templates/radvd.conf.j2 b/roles/radvd/templates/radvd.conf.j2 new file mode 100644 index 0000000..80cdf6d --- /dev/null +++ b/roles/radvd/templates/radvd.conf.j2 @@ -0,0 +1,24 @@ +# {{ ansible_managed }} +{% for iface in radvd_interfaces | default([]) %} +interface {{ iface.name }} { + AdvSendAdvert on; + MinRtrAdvInterval 5; + MaxRtrAdvInterval 15; + route ::/0 {}; + AdvManagedFlag off; + AdvOtherConfigFlag off; + prefix {{ iface.prefix }} { + AdvOnLink on; + AdvAutonomous on; + AdvRouterAddr on; + }; +{% if iface.source is defined %} + AdvRASrcAddress { + {{ iface.source }}; + }; +{% endif %} + RDNSS {{ iface.dns | default(['2a00:1398::1', '2a00:1398::2']) | join(' ') }} { + AdvRDNSSLifetime {{ iface.dns_lifetime | default(3600) }}; + }; +}; +{% endfor %} diff --git a/roles/radvd/vars/debian.yml b/roles/radvd/vars/debian.yml new file mode 100644 index 0000000..34e40e0 --- /dev/null +++ b/roles/radvd/vars/debian.yml @@ -0,0 +1,3 @@ +radvd_os_supported: true +radvd_packages: + - radvd diff --git a/roles/radvd/vars/main.yml b/roles/radvd/vars/main.yml new file mode 100644 index 0000000..9b2b6f2 --- /dev/null +++ b/roles/radvd/vars/main.yml @@ -0,0 +1 @@ +bird_os_supported: false diff --git a/roles/router_bundle/filter_plugins/router_filters.py b/roles/router_bundle/filter_plugins/router_filters.py new file mode 100644 index 0000000..7f560df --- /dev/null +++ b/roles/router_bundle/filter_plugins/router_filters.py @@ -0,0 +1,193 @@ +import ipaddress + +class FilterModule(object): + def filters(self): + return { + 'generate_radvd': self.generate_radvd, + 'generate_keepalived': self.generate_keepalived, + 'generate_interfaces': self.generate_interfaces, + 'generate_nftables': self.generate_nftables, + 'generate_dhcpd': self.generate_dhcpd, + 'generate_ospf': self.generate_ospf, + } + + def generate_radvd(self, cluster_networks): + result = [] + for net in cluster_networks: + if 'prefix' not in net: + continue + if 'dns' in net: + dns = net['dns'] + else: + dns = ['2a00:1398::64:1', '2a00:1398::64:2'] + radvd = { + 'name': net['interface'], + 'prefix': net['prefix'], + 'source': 'fe80::1', + 'dns': dns, + } + result.append(radvd) + return result + + def generate_keepalived(self, cluster_networks): + result = [] + for net in cluster_networks: + keepalived = { + 'name': net['interface'], + 'addresses': ['fe80::1/64'], + } + result.append(keepalived) + if 'prefix_v4' in net: + keepalived = { + 'name': net['interface'] + '_v4', + 'interface': net['interface'], + 'addresses': [self.nth_addr_v4(net['prefix_v4'], 1)], + } + result.append(keepalived) + return result + + def nth_addr(self, prefix: str, n:int) -> str: + network = ipaddress.IPv6Network(prefix) + # Check if n is within the valid range + if n >= network.num_addresses or n < 0: + raise ValueError(f"n must be between 0 and {network.num_addresses - 1}") + # Calculate the n-th address + nth_address = network.network_address + n + return str(nth_address) + '/' + str(network.prefixlen) + + def nth_addr_v4(self, prefix: str, n:int, with_prefix: bool = True) -> str: + network = ipaddress.IPv4Network(prefix) + # Check if n is within the valid range + if n >= network.num_addresses or n < 0: + raise ValueError(f"n must be between 0 and {network.num_addresses - 1}") + # Calculate the n-th address + nth_address = network.network_address + n + if with_prefix: + return str(nth_address) + '/' + str(network.prefixlen) + else: + return str(nth_address) + + def generate_interfaces(self, cluster_networks, addr_num): + result = [] + for net in cluster_networks: + interface = { + 'name': net['interface'], + 'mac': net['mac'], + } + if 'prefix' in net: + inet6 = { + 'addr': [self.nth_addr(net['prefix'], addr_num)], + 'gateway': False, + 'custom': False, + 'routes': False, + } + interface['inet6'] = inet6 + if 'prefix_v4' in net: + inet4 = { + 'addr': [self.nth_addr_v4(net['prefix_v4'], addr_num)], + 'gateway': False, + 'custom': False, + 'routes': False, + } + interface['inet'] = inet4 + result.append(interface) + return result + + def clean_port_range(self, port_range): + if ('{' not in port_range) and ('-' in port_range or ',' in port_range): + return '{{' + port_range + '}}' + return port_range + + def generate_nftables(self, cluster_networks, upstream_interface): + result = [] + nat_rules = [] + default_policies = [] + for net in cluster_networks: + if 'prefix' in net: + if net.get('firewall', None) is not None and net['firewall'].get('enabled', False) == True: + for rule in net['firewall'].get('rules', []): + if rule.get('dst', None) is None: + rule['dst'] = '%net' + + nft_rule = 'oifname {} '.format(net['interface']) + if rule.get('src', '') != '': + nft_rule += 'ip6 saddr {} '.format(rule['src'].replace('%net', net['prefix'])) + if rule.get('dst', '') != '': + nft_rule += 'ip6 daddr {} '.format(rule['dst'].replace('%net', net['prefix'])) + if rule.get('sport', '') != '': + nft_rule += '{{proto}} sport {} '.format(self.clean_port_range(rule['sport'])) + if rule.get('dport', '') != '': + nft_rule += '{{proto}} dport {} '.format(self.clean_port_range(rule['dport'])) + nft_rule += rule['action'] + for proto in rule.get('protocols', ['tcp', 'udp']): + result.append(nft_rule.format(proto=proto)) + default_policies.append('oifname {} ip6 daddr {} {}'.format(net['interface'], net['prefix'], net['firewall']['policy'])) + else: + default_policies.append('oifname {} ip6 daddr {} {}'.format(net['interface'], net['prefix'], 'accept')) + # NAT + if net.get('nat', None) is not None and net['nat'].get('enabled', False) == True: + if net['nat'].get('source', None) is not None and net.get('prefix_v4', None) is not None: + nat_rules.append('ip saddr {prefix} oif {dest_iface} snat to {source}'.format(prefix=net['prefix_v4'], dest_iface=upstream_interface, source=net['nat']['source'])) + + default_policies.append('oifname {} accept'.format(upstream_interface)) + + full_rules = "\n".join(result + default_policies) + + full_nat = "\n".join(nat_rules) + + ret = [ + { + 'name': 'cluster_router_bundle', + 'rule': full_rules, + 'chain': 'forward', + }, + { + 'name': 'cluster_router_bundle', + 'rule': full_nat, + 'chain': 'postrouting', + } + ] + return ret + + def is_v4(self, addr: str) -> bool: + try: + ipaddress.IPv4Address(addr) + return True + except ipaddress.AddressValueError: + return False + + def generate_dhcpd(self, cluster_networks): + result = [] + for net in cluster_networks: + if net.get('prefix_v4', None) is None: + continue + if net.get('dhcp', None) is None or net['dhcp'].get('enabled', False) == False: + continue + router = self.nth_addr_v4(net['prefix_v4'], 1, False) + subnet = self.nth_addr_v4(net['prefix_v4'], 0, False) + netmask = str(ipaddress.IPv4Network(net['prefix_v4']).netmask) + range_start = self.nth_addr_v4(net['prefix_v4'], net['dhcp'].get('range_start', 20), False) + range_end = self.nth_addr_v4(net['prefix_v4'], net['dhcp'].get('range_end', 150), False) + dhcpd = { + 'interface': net['interface'], + 'subnet': subnet, + 'router': router, + 'netmask': netmask, + 'range_start': range_start, + 'range_end': range_end, + } + dns = ', '.join(list(filter(lambda x: self.is_v4(x), net.get('dns', [])))) + if len(dns) > 0: + dhcpd['dns'] = dns + result.append(dhcpd) + return result + + def generate_ospf(self, cluster_networks): + result = [] + for net in cluster_networks: + ospf = { + 'name': net['interface'], + 'stub': True, + } + result.append(ospf) + return result diff --git a/roles/router_bundle/tasks/main.yml b/roles/router_bundle/tasks/main.yml new file mode 100644 index 0000000..c72b0ee --- /dev/null +++ b/roles/router_bundle/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Generate radvd config + set_fact: + radvd_interfaces: "{{ cluster_networks | generate_radvd | union(radvd_interfaces | default([])) }}" +- name: Generate keepalived config + set_fact: + keepalived_interfaces: "{{ cluster_networks | generate_keepalived | union(keepalived_interfaces | default([])) }}" +- name: Generate interfaces config + set_fact: + net_interfaces: "{{ cluster_networks | generate_interfaces(cluster_v6_end) | union(net_interfaces | default([])) }}" +- name: Generate firewall config + set_fact: + nftables_rules: "{{ cluster_networks | generate_nftables(cluster_upstream_interface) | union(nftables_rules | default([])) }}" +- name: Generate dhcp config + set_fact: + isc_dhcp_networks: "{{ cluster_networks | generate_dhcpd | union(isc_dhcp_networks | default([])) }}" + # - name: Generate ospf config + # set_fact: + # bird_ospf_interfaces: "{{ cluster_networks | generate_ospf | union(bird_ospf_interfaces | default([])) }}"