summaryrefslogtreecommitdiff
path: root/roles/vrf_firewall
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-10-05 20:56:05 +1000
committeromnom62 <omnom62@outlook.com>2026-10-05 20:56:05 +1000
commit8ee802e41374942965b6b93cfb4534725ef45145 (patch)
treeef3e5579bd767422caa6fe77cea6a7b5d2d8562c /roles/vrf_firewall
parent748df2bc1d35fa285dd3fe46916e1230408778c7 (diff)
downloadvyos.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.yml11
-rw-r--r--roles/vrf_firewall/meta/argument_specs.yml139
-rw-r--r--roles/vrf_firewall/meta/main.yml11
-rw-r--r--roles/vrf_firewall/tasks/main.yml60
-rw-r--r--roles/vrf_firewall/tasks/verify.yml26
-rw-r--r--roles/vrf_firewall/templates/firewall_rules.yml.j245
-rw-r--r--roles/vrf_firewall/templates/vrf.yml.j223
-rw-r--r--roles/vrf_firewall/templates/vrf_cli.j215
-rw-r--r--roles/vrf_firewall/vars/main.yml24
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 }}"