diff options
| author | omnom62 <omnom62@outlook.com> | 2026-10-05 20:56:05 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-10-05 20:56:05 +1000 |
| commit | 8ee802e41374942965b6b93cfb4534725ef45145 (patch) | |
| tree | ef3e5579bd767422caa6fe77cea6a7b5d2d8562c /roles/vrf_firewall | |
| parent | 748df2bc1d35fa285dd3fe46916e1230408778c7 (diff) | |
| download | vyos.blueprints-main.tar.gz vyos.blueprints-main.zip | |
T9393: new rolesmain
Diffstat (limited to 'roles/vrf_firewall')
| -rw-r--r-- | roles/vrf_firewall/defaults/main.yml | 11 | ||||
| -rw-r--r-- | roles/vrf_firewall/meta/argument_specs.yml | 139 | ||||
| -rw-r--r-- | roles/vrf_firewall/meta/main.yml | 11 | ||||
| -rw-r--r-- | roles/vrf_firewall/tasks/main.yml | 60 | ||||
| -rw-r--r-- | roles/vrf_firewall/tasks/verify.yml | 26 | ||||
| -rw-r--r-- | roles/vrf_firewall/templates/firewall_rules.yml.j2 | 45 | ||||
| -rw-r--r-- | roles/vrf_firewall/templates/vrf.yml.j2 | 23 | ||||
| -rw-r--r-- | roles/vrf_firewall/templates/vrf_cli.j2 | 15 | ||||
| -rw-r--r-- | roles/vrf_firewall/vars/main.yml | 24 |
9 files changed, 354 insertions, 0 deletions
diff --git a/roles/vrf_firewall/defaults/main.yml b/roles/vrf_firewall/defaults/main.yml new file mode 100644 index 0000000..204728e --- /dev/null +++ b/roles/vrf_firewall/defaults/main.yml @@ -0,0 +1,11 @@ +--- +vrf_firewall_bind_to_all: true +vrf_firewall_forward_rules: [] +vrf_firewall_forward_default_action: drop +vrf_firewall_input_rules: [] +vrf_firewall_input_default_action: drop +vrf_firewall_default_log: true +vrf_firewall_state_policy: + established: accept + related: accept + invalid: drop diff --git a/roles/vrf_firewall/meta/argument_specs.yml b/roles/vrf_firewall/meta/argument_specs.yml new file mode 100644 index 0000000..91f569d --- /dev/null +++ b/roles/vrf_firewall/meta/argument_specs.yml @@ -0,0 +1,139 @@ +--- +argument_specs: + main: + short_description: VRFs with route leaking and firewall (docs blueprint "VRF and firewall example") + description: + - Creates VRFs with their routing tables, puts interfaces (including + VLANs) into them, adds static routes including routes into another VRF + (route leaking), sets the global state policy and configures the IPv4 + forward and input filters. + - Interface addressing belongs to C(vyos.blueprints.base); PPPoE is not + configured here. + - VLAN, PPPoE and other non-ethernet VRF bindings and inter-VRF routes + use vyos.vyos.vyos_config, because vyos_interfaces and vyos_vrf cannot + express them yet. Everything else uses resource modules. + - In firewall rules, match an interface in a non-default VRF by the VRF + name for C(inbound_interface) and by the interface name for + C(outbound_interface), as the docs page explains. + - B(Lockout risk) - the input filter defaults to drop. Allow the + interface or VRF you manage the router through before running the role. + options: + vrf_firewall_vrfs: + type: list + elements: dict + required: true + description: VRFs. + options: + name: + type: str + required: true + description: VRF name, e.g. C(MGMT). + table: + type: int + required: true + description: Routing table id. + description: + type: str + description: VRF description. + interfaces: + type: list + elements: str + default: [] + description: Member interfaces, e.g. C(eth1), C(eth2.150), C(pppoe0). + routes: + type: list + elements: dict + default: [] + description: IPv4 static routes in this VRF. + options: + dest: + type: str + required: true + description: Destination prefix. + interface: + type: str + description: Outgoing interface. + vrf: + type: str + description: VRF the interface belongs to, for routes into another VRF. + next_hop: + type: str + description: Next-hop address (routes within this VRF only). + vrf_firewall_bind_to_all: + type: bool + default: true + description: Set C(vrf bind-to-all), as on the docs page. + vrf_firewall_forward_rules: + type: list + elements: dict + default: [] + description: Rules for traffic between VRFs (C(firewall ipv4 forward filter)). + options: &rule + number: + type: int + required: true + description: Rule number. + action: + type: str + choices: [accept, drop, reject] + default: accept + description: Rule action. + description: + type: str + description: Rule description. + inbound_interface: + type: str + description: Inbound interface or VRF name; wildcards like C(eth2*) allowed. + outbound_interface: + type: str + description: Outbound interface name; wildcards allowed. + protocol: + type: str + description: Protocol. + source: + type: dict + description: Source match (C(address), C(port)). + destination: + type: dict + description: Destination match (C(address), C(port)). + log: + type: bool + default: false + description: Log matches. + vrf_firewall_forward_default_action: + type: str + choices: [accept, drop, reject] + default: drop + description: Forward filter default action. + vrf_firewall_input_rules: + type: list + elements: dict + default: [] + description: Rules for traffic to the router itself (C(firewall ipv4 input filter)). + options: *rule + vrf_firewall_input_default_action: + type: str + choices: [accept, drop, reject] + default: drop + description: Input filter default action. + vrf_firewall_default_log: + type: bool + default: true + description: Log packets hitting the filters' default action. + vrf_firewall_state_policy: + type: dict + default: {established: accept, related: accept, invalid: drop} + description: Global state policy, connection type to action. + vyos_blueprints_render_only: + type: bool + default: false + description: Collect commands into C(vyos_blueprints_rendered) instead of configuring. + verify: + short_description: Post-deployment checks for the vrf_firewall role + description: Run with C(tasks_from=verify). Checks every VRF exists with its member interfaces. + options: + vrf_firewall_vrfs: + type: list + elements: dict + required: true + description: Same value as for C(main). diff --git a/roles/vrf_firewall/meta/main.yml b/roles/vrf_firewall/meta/main.yml new file mode 100644 index 0000000..63f2c52 --- /dev/null +++ b/roles/vrf_firewall/meta/main.yml @@ -0,0 +1,11 @@ +--- +galaxy_info: + author: VyOS maintainers and contributors + description: VRFs with inter-VRF route leaking and forward/input firewall filters + license: GPL-3.0-or-later + min_ansible_version: "2.16" + platforms: + - name: GenericLinux + versions: [all] + galaxy_tags: [vyos, networking, vrf, firewall] +dependencies: [] diff --git a/roles/vrf_firewall/tasks/main.yml b/roles/vrf_firewall/tasks/main.yml new file mode 100644 index 0000000..2e38f04 --- /dev/null +++ b/roles/vrf_firewall/tasks/main.yml @@ -0,0 +1,60 @@ +--- +- name: Check interface names + ansible.builtin.assert: + that: item.1 is match('^(' ~ (_vrf_firewall_iftypes.keys() | join('|')) ~ ')\d+(\.\d+)?$') + fail_msg: >- + {{ item.1 }} (VRF {{ item.0.name }}) is not a supported interface name; + expected one of {{ _vrf_firewall_iftypes.keys() | join(', ') }} followed by a number, optionally .VLAN + quiet: true + loop: "{{ vrf_firewall_vrfs | subelements('interfaces', skip_missing=true) }}" + loop_control: + label: "{{ item.0.name }} {{ item.1 }}" + +- name: Create VRFs and their tables + vyos.vyos.vyos_vrf: + config: "{{ lookup('ansible.builtin.template', 'vrf.yml.j2') | from_yaml }}" + state: "{{ _vrf_firewall_state }}" + register: _vrf_firewall_r_vrf + +- name: Put ethernet interfaces into their VRFs + vyos.vyos.vyos_interfaces: + config: "{{ _vrf_firewall_eth_bindings }}" + state: "{{ _vrf_firewall_state }}" + register: _vrf_firewall_r_eth + when: _vrf_firewall_eth_bindings | length > 0 + +- name: Bind VLAN and other interfaces, add inter-VRF routes + # vyos_interfaces cannot set a VRF on a VLAN, and vyos_vrf static routes + # cannot point into another VRF, so these go through vyos_config. + vyos.vyos.vyos_config: + lines: "{{ _vrf_firewall_cli_lines }}" + register: _vrf_firewall_r_cli + when: + - _vrf_firewall_cli_lines | length > 0 + - not (vyos_blueprints_render_only | default(false) | bool) + +- name: Set the global state policy + vyos.vyos.vyos_firewall_global: + config: + state_policy: >- + {{ vrf_firewall_state_policy | dict2items(key_name='connection_type', value_name='action') }} + state: "{{ _vrf_firewall_state }}" + register: _vrf_firewall_r_state + when: vrf_firewall_state_policy | length > 0 + +- name: Configure the forward and input filters + vyos.vyos.vyos_firewall_rules: + config: "{{ lookup('ansible.builtin.template', 'firewall_rules.yml.j2') | from_yaml }}" + state: "{{ _vrf_firewall_state }}" + register: _vrf_firewall_r_fw + +- name: Collect rendered commands # noqa: var-naming[no-role-prefix] - shared across roles by design + ansible.builtin.set_fact: + vyos_blueprints_rendered: >- + {{ vyos_blueprints_rendered | default([]) + + ([_vrf_firewall_r_vrf, _vrf_firewall_r_eth] + | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) + + _vrf_firewall_cli_lines + + ([_vrf_firewall_r_state, _vrf_firewall_r_fw] + | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) }} + when: vyos_blueprints_render_only | default(false) | bool diff --git a/roles/vrf_firewall/tasks/verify.yml b/roles/vrf_firewall/tasks/verify.yml new file mode 100644 index 0000000..1732ced --- /dev/null +++ b/roles/vrf_firewall/tasks/verify.yml @@ -0,0 +1,26 @@ +--- +- name: Read VRFs + vyos.vyos.vyos_command: + commands: + - show vrf + register: _vrf_firewall_v + +- name: Every VRF exists + ansible.builtin.assert: + that: _vrf_firewall_v.stdout[0] is search('(?m)^\s*' ~ (item.name | regex_escape) ~ '\s') + fail_msg: "VRF {{ item.name }} is missing from 'show vrf'" + quiet: true + loop: "{{ vrf_firewall_vrfs }}" + loop_control: + label: "{{ item.name }}" + +- name: Every interface is in its VRF + ansible.builtin.assert: + that: >- + _vrf_firewall_v.stdout[0] is search('(?m)^\s*' ~ (item.0.name | regex_escape) + ~ '\s.*\b' ~ (item.1 | regex_escape) ~ '\b') + fail_msg: "{{ item.1 }} is not a member of VRF {{ item.0.name }}" + quiet: true + loop: "{{ vrf_firewall_vrfs | subelements('interfaces', skip_missing=true) }}" + loop_control: + label: "{{ item.0.name }} {{ item.1 }}" diff --git a/roles/vrf_firewall/templates/firewall_rules.yml.j2 b/roles/vrf_firewall/templates/firewall_rules.yml.j2 new file mode 100644 index 0000000..71f5ae0 --- /dev/null +++ b/roles/vrf_firewall/templates/firewall_rules.yml.j2 @@ -0,0 +1,45 @@ +{% macro rules(lst) %} +{% for r in lst %} + - number: {{ r.number | int }} + action: {{ r.action | default('accept') }} +{% if r.description is defined %} + description: {{ r.description | to_json }} +{% endif %} +{% if r.protocol is defined %} + protocol: {{ r.protocol | to_json }} +{% endif %} +{% if r.log | default(false) | bool %} + log: enable +{% endif %} +{% for side in ['inbound_interface', 'outbound_interface'] if r[side] is defined %} + {{ side }}: + name: {{ r[side] | to_json }} +{% endfor %} +{% for side in ['source', 'destination'] if r[side] is defined and r[side] %} + {{ side }}: +{% for k in ['address', 'port'] if r[side][k] is defined %} + {{ k }}: {{ r[side][k] | string | to_json }} +{% endfor %} +{% endfor %} +{% endfor %} +{% endmacro %} +- afi: ipv4 + rule_sets: + - filter: forward + default_action: {{ vrf_firewall_forward_default_action }} +{% if vrf_firewall_default_log | bool %} + enable_default_log: true +{% endif %} +{% if vrf_firewall_forward_rules %} + rules: +{{ rules(vrf_firewall_forward_rules) -}} +{% endif %} + - filter: input + default_action: {{ vrf_firewall_input_default_action }} +{% if vrf_firewall_default_log | bool %} + enable_default_log: true +{% endif %} +{% if vrf_firewall_input_rules %} + rules: +{{ rules(vrf_firewall_input_rules) -}} +{% endif %} diff --git a/roles/vrf_firewall/templates/vrf.yml.j2 b/roles/vrf_firewall/templates/vrf.yml.j2 new file mode 100644 index 0000000..808dde2 --- /dev/null +++ b/roles/vrf_firewall/templates/vrf.yml.j2 @@ -0,0 +1,23 @@ +bind_to_all: {{ vrf_firewall_bind_to_all | bool }} +instances: +{% for v in vrf_firewall_vrfs %} + - name: {{ v.name | to_json }} + table_id: {{ v.table | int }} +{% if v.description is defined %} + description: {{ v.description | to_json }} +{% endif %} +{% set plain = v.routes | default([]) | rejectattr('vrf', 'defined') | list %} +{% if plain %} + protocols: + static: + - address_families: + - afi: ipv4 + routes: +{% for r in plain %} + - dest: {{ r.dest | to_json }} + next_hops: + - {% if r.next_hop is defined %}forward_router_address: {{ r.next_hop | to_json }}{% else %}interface: {{ r.interface | to_json }}{% endif %} + +{% endfor %} +{% endif %} +{% endfor %} diff --git a/roles/vrf_firewall/templates/vrf_cli.j2 b/roles/vrf_firewall/templates/vrf_cli.j2 new file mode 100644 index 0000000..2470e4b --- /dev/null +++ b/roles/vrf_firewall/templates/vrf_cli.j2 @@ -0,0 +1,15 @@ +{# VRF bindings for non-plain-ethernet interfaces, and inter-VRF leak routes. #} +{% for v in vrf_firewall_vrfs %} +{% for i in v.interfaces | default([]) if i is not match('^eth\d+$') %} +{% set m = i | regex_search('^([a-z]+)(\d+)(?:\.(\d+))?$', '\\1', '\\2', '\\3') %} +{% set kind = _vrf_firewall_iftypes[m[0]] %} +{% if m[2] %} +set interfaces {{ kind }} {{ m[0] ~ m[1] }} vif {{ m[2] }} vrf '{{ v.name }}' +{% else %} +set interfaces {{ kind }} {{ i }} vrf '{{ v.name }}' +{% endif %} +{% endfor %} +{% for r in v.routes | default([]) if r.vrf is defined %} +set vrf name {{ v.name }} protocols static route {{ r.dest }} interface {{ r.interface }} vrf '{{ r.vrf }}' +{% endfor %} +{% endfor %} diff --git a/roles/vrf_firewall/vars/main.yml b/roles/vrf_firewall/vars/main.yml new file mode 100644 index 0000000..eb31c3d --- /dev/null +++ b/roles/vrf_firewall/vars/main.yml @@ -0,0 +1,24 @@ +--- +_vrf_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}" +# Interface name prefix -> CLI interface type, for VRF bindings the resource +# modules cannot express (VLANs, PPPoE, tunnels, ...). +_vrf_firewall_iftypes: + eth: ethernet + bond: bonding + br: bridge + dum: dummy + pppoe: pppoe + vtun: openvpn + wg: wireguard + tun: tunnel + vti: vti +# Ethernet interfaces without a VLAN go through vyos_interfaces; the rest via CLI. +_vrf_firewall_eth_bindings: >- + {%- set out = [] -%} + {%- for v in vrf_firewall_vrfs -%} + {%- for i in v.interfaces | default([]) if i is match('^eth\\d+$') -%} + {%- set _ = out.append({'name': i, 'vrf': v.name}) -%} + {%- endfor -%} + {%- endfor -%} + {{ out }} +_vrf_firewall_cli_lines: "{{ (lookup('ansible.builtin.template', 'vrf_cli.j2') or '').splitlines() | select | list }}" |
