summaryrefslogtreecommitdiff
path: root/roles
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
parent748df2bc1d35fa285dd3fe46916e1230408778c7 (diff)
downloadvyos.blueprints-main.tar.gz
vyos.blueprints-main.zip
T9393: new rolesmain
Diffstat (limited to 'roles')
-rw-r--r--roles/bgp_unnumbered/defaults/main.yml7
-rw-r--r--roles/bgp_unnumbered/meta/argument_specs.yml81
-rw-r--r--roles/bgp_unnumbered/meta/main.yml11
-rw-r--r--roles/bgp_unnumbered/tasks/main.yml40
-rw-r--r--roles/bgp_unnumbered/tasks/verify.yml44
-rw-r--r--roles/bgp_unnumbered/templates/bgp_address_family.yml.j211
-rw-r--r--roles/bgp_unnumbered/templates/bgp_global.yml.j27
-rw-r--r--roles/bgp_unnumbered/templates/bgp_neighbors.j214
-rw-r--r--roles/bgp_unnumbered/vars/main.yml3
-rw-r--r--roles/bridge_firewall/defaults/main.yml4
-rw-r--r--roles/bridge_firewall/meta/argument_specs.yml163
-rw-r--r--roles/bridge_firewall/meta/main.yml11
-rw-r--r--roles/bridge_firewall/tasks/main.yml35
-rw-r--r--roles/bridge_firewall/tasks/verify.yml27
-rw-r--r--roles/bridge_firewall/templates/bridge_cli.j272
-rw-r--r--roles/bridge_firewall/templates/ipv4_rules.yml.j254
-rw-r--r--roles/bridge_firewall/vars/main.yml11
-rw-r--r--roles/firewall/defaults/main.yml4
-rw-r--r--roles/firewall/meta/argument_specs.yml77
-rw-r--r--roles/firewall/meta/main.yml11
-rw-r--r--roles/firewall/tasks/main.yml22
-rw-r--r--roles/firewall/tasks/verify.yml23
-rw-r--r--roles/firewall/templates/groups.yml.j242
-rw-r--r--roles/firewall/templates/rules.yml.j217
-rw-r--r--roles/firewall/vars/main.yml3
-rw-r--r--roles/gre_tunnel/defaults/main.yml2
-rw-r--r--roles/gre_tunnel/meta/argument_specs.yml37
-rw-r--r--roles/gre_tunnel/meta/main.yml11
-rw-r--r--roles/gre_tunnel/tasks/main.yml20
-rw-r--r--roles/gre_tunnel/tasks/verify.yml15
-rw-r--r--roles/gre_tunnel/templates/tunnels.j222
-rw-r--r--roles/gre_tunnel/vars/main.yml11
-rw-r--r--roles/ha_vrrp/tasks/main.yml4
-rw-r--r--roles/ipsec_policy_based/defaults/main.yml19
-rw-r--r--roles/ipsec_policy_based/meta/argument_specs.yml145
-rw-r--r--roles/ipsec_policy_based/meta/main.yml11
-rw-r--r--roles/ipsec_policy_based/tasks/main.yml34
-rw-r--r--roles/ipsec_policy_based/tasks/verify.yml23
-rw-r--r--roles/ipsec_policy_based/templates/vpn_ipsec.yml.j258
-rw-r--r--roles/ipsec_policy_based/templates/vpn_ipsec_s2s.yml.j229
-rw-r--r--roles/ipsec_policy_based/vars/main.yml2
-rw-r--r--roles/ipsec_route_based/defaults/main.yml22
-rw-r--r--roles/ipsec_route_based/meta/argument_specs.yml209
-rw-r--r--roles/ipsec_route_based/meta/main.yml11
-rw-r--r--roles/ipsec_route_based/tasks/main.yml113
-rw-r--r--roles/ipsec_route_based/tasks/verify.yml52
-rw-r--r--roles/ipsec_route_based/templates/bgp_address_family.yml.j220
-rw-r--r--roles/ipsec_route_based/templates/bgp_global.yml.j229
-rw-r--r--roles/ipsec_route_based/templates/interface_routes.yml.j25
-rw-r--r--roles/ipsec_route_based/templates/ospf_interfaces.yml.j212
-rw-r--r--roles/ipsec_route_based/templates/ospfv2.yml.j29
-rw-r--r--roles/ipsec_route_based/templates/vpn_ipsec.yml.j251
-rw-r--r--roles/ipsec_route_based/templates/vpn_ipsec_s2s.yml.j227
-rw-r--r--roles/ipsec_route_based/templates/vti.yml.j29
-rw-r--r--roles/ipsec_route_based/templates/vti_cli.j24
-rw-r--r--roles/ipsec_route_based/templates/vti_l3.yml.j25
-rw-r--r--roles/ipsec_route_based/vars/main.yml23
-rw-r--r--roles/nat/defaults/main.yml3
-rw-r--r--roles/nat/meta/argument_specs.yml33
-rw-r--r--roles/nat/meta/main.yml11
-rw-r--r--roles/nat/tasks/main.yml15
-rw-r--r--roles/nat/tasks/verify.yml25
-rw-r--r--roles/nat/vars/main.yml2
-rw-r--r--roles/ospf_unnumbered/defaults/main.yml6
-rw-r--r--roles/ospf_unnumbered/meta/argument_specs.yml68
-rw-r--r--roles/ospf_unnumbered/meta/main.yml11
-rw-r--r--roles/ospf_unnumbered/tasks/main.yml28
-rw-r--r--roles/ospf_unnumbered/tasks/verify.yml42
-rw-r--r--roles/ospf_unnumbered/templates/l3_interfaces.yml.j28
-rw-r--r--roles/ospf_unnumbered/templates/ospf_interfaces.yml.j212
-rw-r--r--roles/ospf_unnumbered/templates/ospfv2.yml.j215
-rw-r--r--roles/ospf_unnumbered/vars/main.yml3
-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
-rw-r--r--roles/zone_firewall/defaults/main.yml6
-rw-r--r--roles/zone_firewall/meta/argument_specs.yml151
-rw-r--r--roles/zone_firewall/meta/main.yml11
-rw-r--r--roles/zone_firewall/tasks/main.yml34
-rw-r--r--roles/zone_firewall/tasks/verify.yml24
-rw-r--r--roles/zone_firewall/templates/firewall_rules.yml.j247
-rw-r--r--roles/zone_firewall/templates/zones.yml.j228
-rw-r--r--roles/zone_firewall/vars/main.yml17
89 files changed, 2790 insertions, 2 deletions
diff --git a/roles/bgp_unnumbered/defaults/main.yml b/roles/bgp_unnumbered/defaults/main.yml
new file mode 100644
index 0000000..cadff8d
--- /dev/null
+++ b/roles/bgp_unnumbered/defaults/main.yml
@@ -0,0 +1,7 @@
+---
+bgp_unnumbered_peer_group: fabric
+bgp_unnumbered_remote_as: external
+bgp_unnumbered_address_families: [ipv4, ipv6]
+bgp_unnumbered_redistribute: [connected]
+bgp_unnumbered_multipath_relax: true
+bgp_unnumbered_group: ""
diff --git a/roles/bgp_unnumbered/meta/argument_specs.yml b/roles/bgp_unnumbered/meta/argument_specs.yml
new file mode 100644
index 0000000..043e26d
--- /dev/null
+++ b/roles/bgp_unnumbered/meta/argument_specs.yml
@@ -0,0 +1,81 @@
+---
+argument_specs:
+ main:
+ short_description: BGP IPv6 unnumbered with extended next-hop (docs blueprint of the same name)
+ description:
+ - Peers over IPv6 link-local addresses on each listed interface, carries
+ IPv4 and IPv6 with extended next-hop, and redistributes connected
+ routes. Parallel links give ECMP; C(multipath-relax) allows it across
+ different neighbour ASes.
+ - Puts the router-id as a /32 on C(lo), as in the page's results.
+ - AS, router-id and redistribution use vyos.vyos BGP modules. Peer-group,
+ interface (v6only) neighbours and multipath-relax use
+ vyos.vyos.vyos_config, because the BGP resource modules cannot express
+ them yet.
+ - Works for any number of routers; each router lists only its own links.
+ options:
+ bgp_unnumbered_asn:
+ type: int
+ required: true
+ description: Local AS number (C(system-as)).
+ bgp_unnumbered_router_id:
+ type: str
+ required: true
+ description: BGP router-id; also configured as a /32 on C(lo).
+ bgp_unnumbered_interfaces:
+ type: list
+ elements: str
+ required: true
+ description: Interfaces to peer on, e.g. C([eth1, eth2]). They need no IP address.
+ bgp_unnumbered_peer_group:
+ type: str
+ default: fabric
+ description: Peer-group name shared by all unnumbered neighbours.
+ bgp_unnumbered_remote_as:
+ type: str
+ default: external
+ description: C(external), C(internal) or an AS number.
+ bgp_unnumbered_address_families:
+ type: list
+ elements: str
+ choices: [ipv4, ipv6]
+ default: [ipv4, ipv6]
+ description: Address families activated on the peer-group.
+ bgp_unnumbered_redistribute:
+ type: list
+ elements: str
+ default: [connected]
+ description: Route types redistributed in every listed address family.
+ bgp_unnumbered_multipath_relax:
+ type: bool
+ default: true
+ description: Set C(bestpath as-path multipath-relax).
+ bgp_unnumbered_group:
+ type: str
+ default: ""
+ description: Inventory group of all routers in the fabric; used only by C(verify).
+ 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 bgp_unnumbered role
+ description:
+ - Run with C(tasks_from=verify) against all routers in the same play.
+ - Checks one established session per listed interface and, with
+ C(bgp_unnumbered_group), a BGP route to every other router-id. When the
+ group has exactly two routers, also checks one ECMP next hop per link.
+ options:
+ bgp_unnumbered_router_id:
+ type: str
+ required: true
+ description: Same value as for C(main).
+ bgp_unnumbered_interfaces:
+ type: list
+ elements: str
+ required: true
+ description: Same value as for C(main).
+ bgp_unnumbered_group:
+ type: str
+ default: ""
+ description: Same value as for C(main).
diff --git a/roles/bgp_unnumbered/meta/main.yml b/roles/bgp_unnumbered/meta/main.yml
new file mode 100644
index 0000000..af7872b
--- /dev/null
+++ b/roles/bgp_unnumbered/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: eBGP over IPv6 link-local (unnumbered) interfaces with extended next-hop
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, bgp]
+dependencies: []
diff --git a/roles/bgp_unnumbered/tasks/main.yml b/roles/bgp_unnumbered/tasks/main.yml
new file mode 100644
index 0000000..1317f4f
--- /dev/null
+++ b/roles/bgp_unnumbered/tasks/main.yml
@@ -0,0 +1,40 @@
+---
+- name: Put the router-id on the loopback
+ vyos.vyos.vyos_l3_interfaces:
+ config:
+ - name: lo
+ ipv4:
+ - address: "{{ bgp_unnumbered_router_id }}/32"
+ state: "{{ _bgp_unnumbered_state }}"
+ register: _bgp_unnumbered_r_lo
+
+- name: Configure the BGP process (AS, router-id, best-path)
+ vyos.vyos.vyos_bgp_global:
+ config: "{{ lookup('ansible.builtin.template', 'bgp_global.yml.j2') | from_yaml }}"
+ state: "{{ _bgp_unnumbered_state }}"
+ register: _bgp_unnumbered_r_global
+
+- name: Redistribute into each address family
+ vyos.vyos.vyos_bgp_address_family:
+ config: "{{ lookup('ansible.builtin.template', 'bgp_address_family.yml.j2') | from_yaml }}"
+ state: "{{ _bgp_unnumbered_state }}"
+ register: _bgp_unnumbered_r_af
+ when: bgp_unnumbered_redistribute | length > 0
+
+- name: Configure the peer-group and unnumbered neighbours
+ # Not expressible with vyos_bgp_global yet (interface v6only neighbours,
+ # peer-group definitions, remote-as external, multipath-relax).
+ vyos.vyos.vyos_config:
+ lines: "{{ _bgp_unnumbered_cli_lines }}"
+ register: _bgp_unnumbered_r_cli
+ when: not (vyos_blueprints_render_only | default(false) | bool)
+
+- 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([])
+ + ([_bgp_unnumbered_r_lo, _bgp_unnumbered_r_global, _bgp_unnumbered_r_af]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten
+ | unique)
+ + _bgp_unnumbered_cli_lines }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/bgp_unnumbered/tasks/verify.yml b/roles/bgp_unnumbered/tasks/verify.yml
new file mode 100644
index 0000000..cbc98b9
--- /dev/null
+++ b/roles/bgp_unnumbered/tasks/verify.yml
@@ -0,0 +1,44 @@
+---
+- name: Read BGP sessions
+ vyos.vyos.vyos_command:
+ commands:
+ - show bgp summary
+ register: _bgp_unnumbered_v_sum
+
+- name: One established session per interface
+ # An established neighbour shows its Up/Down time followed by a prefix
+ # count; other states show their name (Active, Connect, ...) instead.
+ ansible.builtin.assert:
+ that: _bgp_unnumbered_v_sum.stdout[0] is search('(?m)^' ~ item ~ '\s.*\s(\d{2}:\d{2}:\d{2}|\d+[dwh]\S*)\s+\d+\b')
+ fail_msg: "BGP session on {{ item }} is not established"
+ quiet: true
+ loop: "{{ bgp_unnumbered_interfaces }}"
+
+- name: Read routes to the other routers
+ vyos.vyos.vyos_command:
+ commands: "{{ _bgp_unnumbered_peers | map('regex_replace', '^(.*)$', 'show ip route \\1/32') | list }}"
+ vars:
+ _bgp_unnumbered_peers: >-
+ {{ groups[bgp_unnumbered_group] | difference([inventory_hostname])
+ | map('extract', hostvars, 'bgp_unnumbered_router_id') | list }}
+ register: _bgp_unnumbered_v_routes
+ when: bgp_unnumbered_group | length > 0
+
+- name: Every other router-id is reached via BGP
+ ansible.builtin.assert:
+ that: item is search('Known via "bgp"')
+ fail_msg: "{{ item | regex_search('Routing entry for \\S+') | default('a peer', true) }} is not learned via BGP"
+ quiet: true
+ loop: "{{ _bgp_unnumbered_v_routes.stdout | default([]) }}"
+ loop_control:
+ label: "{{ item | regex_search('Routing entry for \\S+') | default('route', true) }}"
+ when: bgp_unnumbered_group | length > 0
+
+- name: With two routers, one ECMP next hop per link
+ ansible.builtin.assert:
+ that: _bgp_unnumbered_v_routes.stdout[0] | regex_findall('fe80::\S+, via \S+') | length == bgp_unnumbered_interfaces | length
+ fail_msg: "expected {{ bgp_unnumbered_interfaces | length }} ECMP next hops to the peer"
+ quiet: true
+ when:
+ - bgp_unnumbered_group | length > 0
+ - groups[bgp_unnumbered_group] | length == 2
diff --git a/roles/bgp_unnumbered/templates/bgp_address_family.yml.j2 b/roles/bgp_unnumbered/templates/bgp_address_family.yml.j2
new file mode 100644
index 0000000..552f1ae
--- /dev/null
+++ b/roles/bgp_unnumbered/templates/bgp_address_family.yml.j2
@@ -0,0 +1,11 @@
+as_number: {{ bgp_unnumbered_asn | int }}
+address_family:
+{% for afi in bgp_unnumbered_address_families %}
+ - afi: {{ afi }}
+{% if bgp_unnumbered_redistribute %}
+ redistribute:
+{% for r in bgp_unnumbered_redistribute %}
+ - protocol: {{ r }}
+{% endfor %}
+{% endif %}
+{% endfor %}
diff --git a/roles/bgp_unnumbered/templates/bgp_global.yml.j2 b/roles/bgp_unnumbered/templates/bgp_global.yml.j2
new file mode 100644
index 0000000..4c9c633
--- /dev/null
+++ b/roles/bgp_unnumbered/templates/bgp_global.yml.j2
@@ -0,0 +1,7 @@
+as_number: {{ bgp_unnumbered_asn | int }}
+bgp_params:
+ router_id: {{ bgp_unnumbered_router_id | to_json }}
+ bestpath:
+ compare_routerid: true
+ default:
+ no_ipv4_unicast: true
diff --git a/roles/bgp_unnumbered/templates/bgp_neighbors.j2 b/roles/bgp_unnumbered/templates/bgp_neighbors.j2
new file mode 100644
index 0000000..34565ac
--- /dev/null
+++ b/roles/bgp_unnumbered/templates/bgp_neighbors.j2
@@ -0,0 +1,14 @@
+{% set pg = bgp_unnumbered_peer_group %}
+{% set base = 'set protocols bgp' %}
+{% for afi in bgp_unnumbered_address_families %}
+{{ base }} peer-group {{ pg }} address-family {{ afi }}-unicast
+{% endfor %}
+{{ base }} peer-group {{ pg }} capability extended-nexthop
+{{ base }} peer-group {{ pg }} remote-as '{{ bgp_unnumbered_remote_as }}'
+{% for i in bgp_unnumbered_interfaces %}
+{{ base }} neighbor {{ i }} interface v6only
+{{ base }} neighbor {{ i }} interface v6only peer-group '{{ pg }}'
+{% endfor %}
+{% if bgp_unnumbered_multipath_relax | bool %}
+{{ base }} parameters bestpath as-path multipath-relax
+{% endif %}
diff --git a/roles/bgp_unnumbered/vars/main.yml b/roles/bgp_unnumbered/vars/main.yml
new file mode 100644
index 0000000..64eb708
--- /dev/null
+++ b/roles/bgp_unnumbered/vars/main.yml
@@ -0,0 +1,3 @@
+---
+_bgp_unnumbered_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_bgp_unnumbered_cli_lines: "{{ (lookup('ansible.builtin.template', 'bgp_neighbors.j2') or '').splitlines() | select | list }}"
diff --git a/roles/bridge_firewall/defaults/main.yml b/roles/bridge_firewall/defaults/main.yml
new file mode 100644
index 0000000..f656cb5
--- /dev/null
+++ b/roles/bridge_firewall/defaults/main.yml
@@ -0,0 +1,4 @@
+---
+bridge_firewall_state_rules: true
+bridge_firewall_bridge_forward_default_action: drop
+bridge_firewall_ipv4_forward_default_action: drop
diff --git a/roles/bridge_firewall/meta/argument_specs.yml b/roles/bridge_firewall/meta/argument_specs.yml
new file mode 100644
index 0000000..6d5493e
--- /dev/null
+++ b/roles/bridge_firewall/meta/argument_specs.yml
@@ -0,0 +1,163 @@
+---
+argument_specs:
+ main:
+ short_description: Bridges with bridge and IPv4 firewall (docs blueprint "Bridge and firewall example")
+ description:
+ - Per bridge, creates the bridge and its members, an interface group
+ C(<bridge>-ifaces), bridge-firewall rulesets C(<bridge>-pre) (prerouting)
+ and C(<bridge>-fwd) (forward) with the jump rules that select them, and
+ optionally an IPv4 ruleset C(ip-<bridge>-fwd) for routed traffic and an
+ input rule for access to the router - the structure used on the docs page.
+ - Bridges, interface groups and the bridge firewall use
+ vyos.vyos.vyos_config, because vyos.vyos has no resource modules for
+ them yet. Member descriptions and the IPv4 firewall use resource modules.
+ options:
+ bridge_firewall_bridges:
+ type: list
+ elements: dict
+ required: true
+ description: Bridges, in order; their position sets the jump rule numbers (10, 20, ... and 110, 120, ...).
+ options:
+ name:
+ type: str
+ required: true
+ description: Bridge interface, e.g. C(br1).
+ description:
+ type: str
+ description: Bridge description.
+ addresses:
+ type: list
+ elements: str
+ default: []
+ description: Bridge addresses; omit for a pure layer-2 bridge.
+ members:
+ type: list
+ elements: dict
+ required: true
+ description: Member interfaces.
+ options:
+ interface:
+ type: str
+ required: true
+ description: Member interface, e.g. C(eth3).
+ description:
+ type: str
+ description: Member interface description.
+ prerouting:
+ type: dict
+ description: Bridge ruleset C(<bridge>-pre), applied in the prerouting chain.
+ options: &bridge_ruleset
+ default_action:
+ type: str
+ choices: [accept, drop]
+ description: Ruleset default action (C(accept) for prerouting, C(drop) for forward by default).
+ rules:
+ type: list
+ elements: dict
+ default: []
+ description: Bridge firewall rules.
+ options:
+ number:
+ type: int
+ required: true
+ description: Rule number.
+ action:
+ type: str
+ choices: [accept, drop]
+ default: accept
+ description: Rule action.
+ description:
+ type: str
+ description: Rule description.
+ ethernet_type:
+ type: str
+ description: Ethernet type, e.g. C(ipv4), C(ipv6), C(arp).
+ protocol:
+ type: str
+ description: IP protocol, e.g. C(udp).
+ source:
+ type: dict
+ description: Source match (C(address), C(port), C(mac_address)).
+ destination:
+ type: dict
+ description: Destination match (C(address), C(port), C(mac_address)).
+ inbound_interface:
+ type: str
+ description: Inbound bridge port, e.g. C(eth6).
+ state:
+ type: list
+ elements: str
+ default: []
+ description: Connection states, e.g. C([new]).
+ log:
+ type: bool
+ default: false
+ description: Log matches.
+ forward:
+ type: dict
+ description: Bridge ruleset C(<bridge>-fwd), applied in the bridge forward chain.
+ options: *bridge_ruleset
+ ip_forward:
+ type: dict
+ description: IPv4 ruleset C(ip-<bridge>-fwd) for traffic routed out of the bridge.
+ options:
+ default_action:
+ type: str
+ choices: [accept, drop, reject]
+ default: drop
+ description: Ruleset default action.
+ rules:
+ type: list
+ elements: dict
+ default: []
+ description: IPv4 rules.
+ options:
+ 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.
+ outbound_interface:
+ type: str
+ description: Outbound interface, e.g. the internet uplink.
+ outbound_bridge:
+ type: str
+ description: Another bridge from this list, matched via its interface group.
+ router_access:
+ type: str
+ choices: [accept, drop]
+ description: Accept or drop IPv4 traffic from this bridge to the router itself.
+ bridge_firewall_state_rules:
+ type: bool
+ default: true
+ description: Add the page's established/related and invalid rules to the forward and input chains.
+ bridge_firewall_bridge_forward_default_action:
+ type: str
+ choices: [accept, drop]
+ default: drop
+ description: Default action of the bridge forward chain.
+ bridge_firewall_ipv4_forward_default_action:
+ type: str
+ choices: [accept, drop, reject]
+ default: drop
+ description: Default action of the IPv4 forward chain.
+ 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 bridge_firewall role
+ description: Run with C(tasks_from=verify). Checks bridge membership and that the bridge rulesets are active.
+ options:
+ bridge_firewall_bridges:
+ type: list
+ elements: dict
+ required: true
+ description: Same value as for C(main).
diff --git a/roles/bridge_firewall/meta/main.yml b/roles/bridge_firewall/meta/main.yml
new file mode 100644
index 0000000..4e556cb
--- /dev/null
+++ b/roles/bridge_firewall/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Bridges with bridge-layer and IPv4 firewall policies per bridge
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, bridge, firewall]
+dependencies: []
diff --git a/roles/bridge_firewall/tasks/main.yml b/roles/bridge_firewall/tasks/main.yml
new file mode 100644
index 0000000..d947258
--- /dev/null
+++ b/roles/bridge_firewall/tasks/main.yml
@@ -0,0 +1,35 @@
+---
+- name: Set member interface descriptions
+ vyos.vyos.vyos_interfaces:
+ config: "{{ _bridge_firewall_member_descriptions }}"
+ state: "{{ _bridge_firewall_state }}"
+ register: _bridge_firewall_r_ifaces
+ when: _bridge_firewall_member_descriptions | length > 0
+
+- name: Create bridges, interface groups and the bridge firewall
+ # No vyos.vyos resource module covers bridges, interface groups or the
+ # bridge firewall yet, so these go through vyos_config.
+ vyos.vyos.vyos_config:
+ lines: "{{ _bridge_firewall_cli_lines }}"
+ register: _bridge_firewall_r_cli
+ when: not (vyos_blueprints_render_only | default(false) | bool)
+
+- name: Configure the IPv4 input and forward filters
+ vyos.vyos.vyos_firewall_rules:
+ config: "{{ _bridge_firewall_ipv4 }}"
+ state: "{{ _bridge_firewall_state }}"
+ vars:
+ _bridge_firewall_ipv4: "{{ lookup('ansible.builtin.template', 'ipv4_rules.yml.j2') | from_yaml }}"
+ register: _bridge_firewall_r_ipv4
+ when: >-
+ bridge_firewall_bridges | selectattr('ip_forward', 'defined') | list | length > 0
+ or bridge_firewall_bridges | selectattr('router_access', 'defined') | list | length > 0
+
+- 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([])
+ + (_bridge_firewall_r_ifaces.rendered | default([]))
+ + _bridge_firewall_cli_lines
+ + (_bridge_firewall_r_ipv4.rendered | default([])) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/bridge_firewall/tasks/verify.yml b/roles/bridge_firewall/tasks/verify.yml
new file mode 100644
index 0000000..295b8af
--- /dev/null
+++ b/roles/bridge_firewall/tasks/verify.yml
@@ -0,0 +1,27 @@
+---
+- name: Read bridges and bridge firewall
+ vyos.vyos.vyos_command:
+ commands:
+ - show configuration commands
+ - show firewall bridge
+ register: _bridge_firewall_v
+
+- name: Every member interface is in its bridge
+ ansible.builtin.assert:
+ that: >-
+ _bridge_firewall_v.stdout[0] is search('set interfaces bridge ' ~ (item.0.name | regex_escape)
+ ~ ' member interface ' ~ (item.1.interface | regex_escape) ~ '\b')
+ fail_msg: "{{ item.1.interface }} is not a member of {{ item.0.name }}"
+ quiet: true
+ loop: "{{ bridge_firewall_bridges | subelements('members') }}"
+ loop_control:
+ label: "{{ item.0.name }} {{ item.1.interface }}"
+
+- name: Every bridge ruleset is active
+ ansible.builtin.assert:
+ that: _bridge_firewall_v.stdout[1] is search('name ' ~ item)
+ fail_msg: "bridge ruleset {{ item }} is not active"
+ quiet: true
+ loop: >-
+ {{ (bridge_firewall_bridges | selectattr('prerouting', 'defined') | map(attribute='name') | map('regex_replace', '$', '-pre') | list)
+ + (bridge_firewall_bridges | selectattr('forward', 'defined') | map(attribute='name') | map('regex_replace', '$', '-fwd') | list) }}
diff --git a/roles/bridge_firewall/templates/bridge_cli.j2 b/roles/bridge_firewall/templates/bridge_cli.j2
new file mode 100644
index 0000000..18cb1f3
--- /dev/null
+++ b/roles/bridge_firewall/templates/bridge_cli.j2
@@ -0,0 +1,72 @@
+{#- Bridges, interface groups and the bridge firewall: no vyos.vyos resource
+ module covers these yet. Rule numbers for the jump rules follow the page. -#}
+{%- macro bridge_rule(prefix, r) -%}
+{{ prefix }} rule {{ r.number }} action '{{ r.action | default('accept') }}'
+{% if r.description is defined %}
+{{ prefix }} rule {{ r.number }} description '{{ r.description }}'
+{% endif %}
+{% for k in ['ethernet_type', 'protocol'] if r[k] is defined %}
+{{ prefix }} rule {{ r.number }} {{ k | replace('_', '-') }} '{{ r[k] }}'
+{% endfor %}
+{% for side in ['source', 'destination'] if r[side] is defined %}
+{% for k in ['address', 'port', 'mac_address'] if r[side][k] is defined %}
+{{ prefix }} rule {{ r.number }} {{ side }} {{ k | replace('_', '-') }} '{{ r[side][k] }}'
+{% endfor %}
+{% endfor %}
+{% if r.inbound_interface is defined %}
+{{ prefix }} rule {{ r.number }} inbound-interface name '{{ r.inbound_interface }}'
+{% endif %}
+{% for s in r.state | default([]) %}
+{{ prefix }} rule {{ r.number }} state '{{ s }}'
+{% endfor %}
+{% if r.log | default(false) | bool %}
+{{ prefix }} rule {{ r.number }} log
+{% endif %}
+{%- endmacro -%}
+{% for b in bridge_firewall_bridges %}
+{% set i = loop.index %}
+{% set grp = b.name ~ '-ifaces' %}
+{% if b.description is defined %}
+set interfaces bridge {{ b.name }} description '{{ b.description }}'
+{% endif %}
+{% for a in b.addresses | default([]) %}
+set interfaces bridge {{ b.name }} address '{{ a }}'
+{% endfor %}
+{% for m in b.members %}
+set interfaces bridge {{ b.name }} member interface {{ m.interface }}
+{% endfor %}
+set firewall group interface-group {{ grp }} interface '{{ b.name }}'
+{% for m in b.members %}
+set firewall group interface-group {{ grp }} interface '{{ m.interface }}'
+{% endfor %}
+{% if b.prerouting is defined %}
+set firewall bridge prerouting filter rule {{ i * 10 }} action 'jump'
+set firewall bridge prerouting filter rule {{ i * 10 }} description '{{ b.name }} traffic'
+set firewall bridge prerouting filter rule {{ i * 10 }} inbound-interface group '{{ grp }}'
+set firewall bridge prerouting filter rule {{ i * 10 }} jump-target '{{ b.name }}-pre'
+set firewall bridge name {{ b.name }}-pre default-action '{{ b.prerouting.default_action | default('accept') }}'
+{% for r in b.prerouting.rules | default([]) %}
+{{ bridge_rule('set firewall bridge name ' ~ b.name ~ '-pre', r) }}
+{% endfor %}
+{% endif %}
+{% if b.forward is defined %}
+set firewall bridge forward filter rule {{ 100 + i * 10 }} action 'jump'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} description '{{ b.name }} traffic'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} inbound-interface group '{{ grp }}'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} jump-target '{{ b.name }}-fwd'
+set firewall bridge name {{ b.name }}-fwd default-action '{{ b.forward.default_action | default('drop') }}'
+{% for r in b.forward.rules | default([]) %}
+{{ bridge_rule('set firewall bridge name ' ~ b.name ~ '-fwd', r) }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% if bridge_firewall_bridges | selectattr('forward', 'defined') | list %}
+{% if bridge_firewall_state_rules | bool %}
+set firewall bridge forward filter rule 5 action 'accept'
+set firewall bridge forward filter rule 5 state 'established'
+set firewall bridge forward filter rule 5 state 'related'
+set firewall bridge forward filter rule 10 action 'drop'
+set firewall bridge forward filter rule 10 state 'invalid'
+{% endif %}
+set firewall bridge forward filter default-action '{{ bridge_firewall_bridge_forward_default_action }}'
+{% endif %}
diff --git a/roles/bridge_firewall/templates/ipv4_rules.yml.j2 b/roles/bridge_firewall/templates/ipv4_rules.yml.j2
new file mode 100644
index 0000000..dec5520
--- /dev/null
+++ b/roles/bridge_firewall/templates/ipv4_rules.yml.j2
@@ -0,0 +1,54 @@
+{#- IPv4 firewall for routed traffic and router access, via vyos_firewall_rules. -#}
+{% set routed = bridge_firewall_bridges | selectattr('ip_forward', 'defined') | list %}
+{% set access = bridge_firewall_bridges | selectattr('router_access', 'defined') | list %}
+- afi: ipv4
+ rule_sets:
+{% if access %}
+ - filter: input
+ rules:
+{% if bridge_firewall_state_rules | bool %}
+ - {number: 10, action: accept, state: {established: true, related: true}}
+ - {number: 20, action: drop, state: {invalid: true}}
+{% endif %}
+{% for b in access %}
+ - number: {{ 100 + loop.index * 10 }}
+ action: {{ 'accept' if b.router_access == 'accept' else 'drop' }}
+ description: {{ ((b.router_access == 'accept') | ternary('Accept access from ', 'Deny access from ') ~ b.name) | to_json }}
+ inbound_interface: {group: {{ (b.name ~ '-ifaces') | to_json }}}
+{% endfor %}
+{% endif %}
+{% if routed %}
+ - filter: forward
+ default_action: {{ bridge_firewall_ipv4_forward_default_action }}
+ rules:
+{% if bridge_firewall_state_rules | bool %}
+ - {number: 5, action: accept, state: {established: true, related: true}}
+ - {number: 10, action: drop, state: {invalid: true}}
+{% endif %}
+{% for b in routed %}
+ - number: {{ 100 + loop.index * 10 }}
+ action: jump
+ description: {{ (b.name ~ ' traffic') | to_json }}
+ inbound_interface: {group: {{ (b.name ~ '-ifaces') | to_json }}}
+ jump_target: {{ ('ip-' ~ b.name ~ '-fwd') | to_json }}
+{% endfor %}
+{% for b in routed %}
+ - name: {{ ('ip-' ~ b.name ~ '-fwd') | to_json }}
+ default_action: {{ b.ip_forward.default_action | default('drop') }}
+{% if b.ip_forward.rules | default([]) %}
+ rules:
+{% for r in b.ip_forward.rules %}
+ - number: {{ r.number | int }}
+ action: {{ r.action | default('accept') }}
+{% if r.description is defined %}
+ description: {{ r.description | to_json }}
+{% endif %}
+{% if r.outbound_interface is defined %}
+ outbound_interface: {name: {{ r.outbound_interface | to_json }}}
+{% elif r.outbound_bridge is defined %}
+ outbound_interface: {group: {{ (r.outbound_bridge ~ '-ifaces') | to_json }}}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/roles/bridge_firewall/vars/main.yml b/roles/bridge_firewall/vars/main.yml
new file mode 100644
index 0000000..7299574
--- /dev/null
+++ b/roles/bridge_firewall/vars/main.yml
@@ -0,0 +1,11 @@
+---
+_bridge_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_bridge_firewall_cli_lines: "{{ (lookup('ansible.builtin.template', 'bridge_cli.j2') or '').splitlines() | map('trim') | select | list }}"
+_bridge_firewall_member_descriptions: >-
+ {%- set out = [] -%}
+ {%- for b in bridge_firewall_bridges -%}
+ {%- for m in b.members if m.description is defined -%}
+ {%- set _ = out.append({'name': m.interface, 'description': m.description}) -%}
+ {%- endfor -%}
+ {%- endfor -%}
+ {{ out }}
diff --git a/roles/firewall/defaults/main.yml b/roles/firewall/defaults/main.yml
new file mode 100644
index 0000000..d5bec3f
--- /dev/null
+++ b/roles/firewall/defaults/main.yml
@@ -0,0 +1,4 @@
+---
+firewall_groups: {}
+firewall_ipv4: {}
+firewall_ipv6: {}
diff --git a/roles/firewall/meta/argument_specs.yml b/roles/firewall/meta/argument_specs.yml
new file mode 100644
index 0000000..d25fe5d
--- /dev/null
+++ b/roles/firewall/meta/argument_specs.yml
@@ -0,0 +1,77 @@
+---
+argument_specs:
+ main:
+ short_description: Firewall groups and base-chain filters
+ description:
+ - Configures firewall network, address and port groups and the
+ C(input), C(forward) and C(output) filters for IPv4 and IPv6.
+ - Building block used by blueprints such as "Policy-Based Site-to-Site
+ VPN and Firewall Configuration"; for zone-based designs use
+ C(vyos.blueprints.zone_firewall).
+ - Rules use the rule format of vyos.vyos.vyos_firewall_rules (number,
+ action, description, protocol, state, source, destination,
+ inbound_interface, outbound_interface, log, ...), passed through unchanged.
+ - B(Lockout risk) - a C(drop) input filter must allow the address or
+ interface you manage the router from.
+ options:
+ firewall_groups:
+ type: dict
+ default: {}
+ description: Firewall groups.
+ options:
+ network:
+ type: list
+ elements: dict
+ default: []
+ description: IPv4 network groups.
+ options:
+ name: {type: str, required: true, description: Group name.}
+ description: {type: str, description: Group description.}
+ networks: {type: list, elements: str, required: true, description: Member networks.}
+ address:
+ type: list
+ elements: dict
+ default: []
+ description: IPv4 address groups.
+ options:
+ name: {type: str, required: true, description: Group name.}
+ description: {type: str, description: Group description.}
+ addresses: {type: list, elements: str, required: true, description: Member addresses or ranges.}
+ port:
+ type: list
+ elements: dict
+ default: []
+ description: Port groups.
+ options:
+ name: {type: str, required: true, description: Group name.}
+ description: {type: str, description: Group description.}
+ ports: {type: list, elements: str, required: true, description: Member ports or ranges.}
+ firewall_ipv4:
+ type: dict
+ default: {}
+ description: IPv4 filters keyed by base chain (C(input), C(forward), C(output)).
+ options: &filters
+ input: &filter
+ type: dict
+ description: Filter for this chain.
+ options:
+ default_action: {type: str, choices: [accept, drop, reject], description: Default action.}
+ default_log: {type: bool, default: false, description: Log packets hitting the default action.}
+ rules: {type: list, elements: dict, default: [], description: Rules in vyos_firewall_rules format.}
+ forward: *filter
+ output: *filter
+ firewall_ipv6:
+ type: dict
+ default: {}
+ description: IPv6 filters keyed by base chain.
+ options: *filters
+ 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 firewall role
+ description: Run with C(tasks_from=verify). Checks every configured filter is active with its rules.
+ options:
+ firewall_ipv4: {type: dict, default: {}, description: Same value as for C(main).}
+ firewall_ipv6: {type: dict, default: {}, description: Same value as for C(main).}
diff --git a/roles/firewall/meta/main.yml b/roles/firewall/meta/main.yml
new file mode 100644
index 0000000..8c38572
--- /dev/null
+++ b/roles/firewall/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Stateful firewall - groups and IPv4/IPv6 input, forward and output filters
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, firewall]
+dependencies: []
diff --git a/roles/firewall/tasks/main.yml b/roles/firewall/tasks/main.yml
new file mode 100644
index 0000000..fe99b15
--- /dev/null
+++ b/roles/firewall/tasks/main.yml
@@ -0,0 +1,22 @@
+---
+- name: Create firewall groups
+ vyos.vyos.vyos_firewall_global:
+ config: "{{ lookup('ansible.builtin.template', 'groups.yml.j2') | from_yaml }}"
+ state: "{{ _firewall_state }}"
+ register: _firewall_r_groups
+ when: firewall_groups | dict2items | map(attribute='value') | select | list | length > 0
+
+- name: Configure the input, forward and output filters
+ vyos.vyos.vyos_firewall_rules:
+ config: "{{ lookup('ansible.builtin.template', 'rules.yml.j2') | from_yaml }}"
+ state: "{{ _firewall_state }}"
+ register: _firewall_r_rules
+ when: (firewall_ipv4 | length > 0) or (firewall_ipv6 | length > 0)
+
+- 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([])
+ + ([_firewall_r_groups, _firewall_r_rules]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/firewall/tasks/verify.yml b/roles/firewall/tasks/verify.yml
new file mode 100644
index 0000000..c2e3281
--- /dev/null
+++ b/roles/firewall/tasks/verify.yml
@@ -0,0 +1,23 @@
+---
+- name: Read the firewall
+ vyos.vyos.vyos_command:
+ commands:
+ - show firewall
+ register: _firewall_v
+
+- name: Every configured filter is active with its rules
+ ansible.builtin.assert:
+ that: >-
+ _firewall_v.stdout[0] is search((item.afi | upper | replace('V', 'v')) ~ ' Firewall "' ~ item.chain ~ ' filter"')
+ fail_msg: "{{ item.afi }} {{ item.chain }} filter is not active"
+ quiet: true
+ loop: >-
+ {%- set out = [] -%}
+ {%- for afi, filters in [('ipv4', firewall_ipv4), ('ipv6', firewall_ipv6)] -%}
+ {%- for chain in ['input', 'forward', 'output'] if filters[chain] is defined and filters[chain] -%}
+ {%- set _ = out.append({'afi': afi, 'chain': chain}) -%}
+ {%- endfor -%}
+ {%- endfor -%}
+ {{ out }}
+ loop_control:
+ label: "{{ item.afi }} {{ item.chain }}"
diff --git a/roles/firewall/templates/groups.yml.j2 b/roles/firewall/templates/groups.yml.j2
new file mode 100644
index 0000000..5641e2a
--- /dev/null
+++ b/roles/firewall/templates/groups.yml.j2
@@ -0,0 +1,42 @@
+group:
+{% if firewall_groups.network | default([]) %}
+ network_group:
+{% for g in firewall_groups.network %}
+ - name: {{ g.name | to_json }}
+ afi: ipv4
+{% if g.description is defined %}
+ description: {{ g.description | to_json }}
+{% endif %}
+ members:
+{% for n in g.networks %}
+ - address: {{ n | to_json }}
+{% endfor %}
+{% endfor %}
+{% endif %}
+{% if firewall_groups.address | default([]) %}
+ address_group:
+{% for g in firewall_groups.address %}
+ - name: {{ g.name | to_json }}
+ afi: ipv4
+{% if g.description is defined %}
+ description: {{ g.description | to_json }}
+{% endif %}
+ members:
+{% for a in g.addresses %}
+ - address: {{ a | to_json }}
+{% endfor %}
+{% endfor %}
+{% endif %}
+{% if firewall_groups.port | default([]) %}
+ port_group:
+{% for g in firewall_groups.port %}
+ - name: {{ g.name | to_json }}
+{% if g.description is defined %}
+ description: {{ g.description | to_json }}
+{% endif %}
+ members:
+{% for p in g.ports %}
+ - port: {{ p | string | to_json }}
+{% endfor %}
+{% endfor %}
+{% endif %}
diff --git a/roles/firewall/templates/rules.yml.j2 b/roles/firewall/templates/rules.yml.j2
new file mode 100644
index 0000000..f746c84
--- /dev/null
+++ b/roles/firewall/templates/rules.yml.j2
@@ -0,0 +1,17 @@
+{% for afi, filters in [('ipv4', firewall_ipv4), ('ipv6', firewall_ipv6)] if filters %}
+- afi: {{ afi }}
+ rule_sets:
+{% for chain in _firewall_filters if filters[chain] is defined and filters[chain] %}
+{% set f = filters[chain] %}
+ - filter: {{ chain }}
+{% if f.default_action is defined %}
+ default_action: {{ f.default_action }}
+{% endif %}
+{% if f.default_log | default(false) | bool %}
+ enable_default_log: true
+{% endif %}
+{% if f.rules | default([]) %}
+ rules: {{ f.rules | to_json }}
+{% endif %}
+{% endfor %}
+{% endfor %}
diff --git a/roles/firewall/vars/main.yml b/roles/firewall/vars/main.yml
new file mode 100644
index 0000000..eebd515
--- /dev/null
+++ b/roles/firewall/vars/main.yml
@@ -0,0 +1,3 @@
+---
+_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_firewall_filters: [input, forward, output]
diff --git a/roles/gre_tunnel/defaults/main.yml b/roles/gre_tunnel/defaults/main.yml
new file mode 100644
index 0000000..2bb0e43
--- /dev/null
+++ b/roles/gre_tunnel/defaults/main.yml
@@ -0,0 +1,2 @@
+---
+gre_tunnel_interfaces: []
diff --git a/roles/gre_tunnel/meta/argument_specs.yml b/roles/gre_tunnel/meta/argument_specs.yml
new file mode 100644
index 0000000..7916ceb
--- /dev/null
+++ b/roles/gre_tunnel/meta/argument_specs.yml
@@ -0,0 +1,37 @@
+---
+argument_specs:
+ main:
+ short_description: Tunnel interfaces (GRE and other encapsulations)
+ description:
+ - Creates C(interfaces tunnel) entries with their encapsulation,
+ endpoints, MTU and TCP MSS clamping, and addresses them.
+ - Building block used by "Site-to-Site IPSec VPN to Cisco using FlexVPN"
+ (GRE protected by IPsec); also usable for plain GRE.
+ - The tunnel itself is created with vyos.vyos.vyos_config in one commit,
+ because vyos_interfaces cannot set encapsulation or endpoints;
+ addresses use vyos_l3_interfaces.
+ options:
+ gre_tunnel_interfaces:
+ type: list
+ elements: dict
+ required: true
+ description: Tunnel interfaces.
+ options:
+ name: {type: str, required: true, description: "Interface name, e.g. C(tun1)."}
+ encapsulation: {type: str, default: gre, description: "Encapsulation, e.g. C(gre), C(gretap), C(ipip)."}
+ source_address: {type: str, description: Local endpoint address.}
+ source_interface: {type: str, description: Local endpoint interface (instead of C(source_address)).}
+ remote: {type: str, description: Remote endpoint address.}
+ mtu: {type: int, description: Interface MTU.}
+ adjust_mss: {type: str, description: "TCP MSS clamping value, e.g. C(1336) or C(clamp-mss-to-pmtu)."}
+ description: {type: str, description: Interface description.}
+ addresses: {type: list, elements: str, default: [], description: IPv4 addresses in CIDR notation.}
+ 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 gre_tunnel role
+ description: Run with C(tasks_from=verify). Checks every tunnel interface is up.
+ options:
+ gre_tunnel_interfaces: {type: list, elements: dict, required: true, description: Same value as for C(main).}
diff --git a/roles/gre_tunnel/meta/main.yml b/roles/gre_tunnel/meta/main.yml
new file mode 100644
index 0000000..e4408b4
--- /dev/null
+++ b/roles/gre_tunnel/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: GRE (and other) tunnel interfaces
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, gre, tunnel]
+dependencies: []
diff --git a/roles/gre_tunnel/tasks/main.yml b/roles/gre_tunnel/tasks/main.yml
new file mode 100644
index 0000000..5f8a90a
--- /dev/null
+++ b/roles/gre_tunnel/tasks/main.yml
@@ -0,0 +1,20 @@
+---
+- name: Create the tunnel interfaces
+ # vyos_interfaces cannot set encapsulation or endpoints, and a tunnel must
+ # be created with them in one commit, so this goes through vyos_config.
+ vyos.vyos.vyos_config:
+ lines: "{{ _gre_tunnel_cli_lines }}"
+ register: _gre_tunnel_r_cli
+ when: not (vyos_blueprints_render_only | default(false) | bool)
+
+- name: Address the tunnel interfaces
+ vyos.vyos.vyos_l3_interfaces:
+ config: "{{ _gre_tunnel_l3 }}"
+ state: "{{ _gre_tunnel_state }}"
+ register: _gre_tunnel_r_l3
+ when: _gre_tunnel_l3 | length > 0
+
+- 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([]) + _gre_tunnel_cli_lines + (_gre_tunnel_r_l3.rendered | default([])) }}"
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/gre_tunnel/tasks/verify.yml b/roles/gre_tunnel/tasks/verify.yml
new file mode 100644
index 0000000..52cb45f
--- /dev/null
+++ b/roles/gre_tunnel/tasks/verify.yml
@@ -0,0 +1,15 @@
+---
+- name: Read interfaces
+ vyos.vyos.vyos_command:
+ commands:
+ - show interfaces
+ register: _gre_tunnel_v
+
+- name: Every tunnel interface is up
+ ansible.builtin.assert:
+ that: _gre_tunnel_v.stdout[0] is search('(?m)^' ~ (item.name | regex_escape) ~ '\s.*\bu/u\b')
+ fail_msg: "tunnel {{ item.name }} is not up"
+ quiet: true
+ loop: "{{ gre_tunnel_interfaces }}"
+ loop_control:
+ label: "{{ item.name }}"
diff --git a/roles/gre_tunnel/templates/tunnels.j2 b/roles/gre_tunnel/templates/tunnels.j2
new file mode 100644
index 0000000..fb23487
--- /dev/null
+++ b/roles/gre_tunnel/templates/tunnels.j2
@@ -0,0 +1,22 @@
+{% for t in gre_tunnel_interfaces %}
+{% set b = 'set interfaces tunnel ' ~ t.name %}
+{{ b }} encapsulation '{{ t.encapsulation | default('gre') }}'
+{% if t.description is defined %}
+{{ b }} description '{{ t.description }}'
+{% endif %}
+{% if t.adjust_mss is defined %}
+{{ b }} ip adjust-mss '{{ t.adjust_mss }}'
+{% endif %}
+{% if t.mtu is defined %}
+{{ b }} mtu '{{ t.mtu }}'
+{% endif %}
+{% if t.remote is defined %}
+{{ b }} remote '{{ t.remote }}'
+{% endif %}
+{% if t.source_address is defined %}
+{{ b }} source-address '{{ t.source_address }}'
+{% endif %}
+{% if t.source_interface is defined %}
+{{ b }} source-interface '{{ t.source_interface }}'
+{% endif %}
+{% endfor %}
diff --git a/roles/gre_tunnel/vars/main.yml b/roles/gre_tunnel/vars/main.yml
new file mode 100644
index 0000000..87be41e
--- /dev/null
+++ b/roles/gre_tunnel/vars/main.yml
@@ -0,0 +1,11 @@
+---
+_gre_tunnel_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_gre_tunnel_cli_lines: "{{ (lookup('ansible.builtin.template', 'tunnels.j2') or '').splitlines() | select | list }}"
+_gre_tunnel_l3: >-
+ {%- set out = [] -%}
+ {%- for t in gre_tunnel_interfaces if t.addresses | default([]) -%}
+ {%- set addrs = [] -%}
+ {%- for a in t.addresses -%}{%- set _ = addrs.append({'address': a}) -%}{%- endfor -%}
+ {%- set _ = out.append({'name': t.name, 'ipv4': addrs}) -%}
+ {%- endfor -%}
+ {{ out }}
diff --git a/roles/ha_vrrp/tasks/main.yml b/roles/ha_vrrp/tasks/main.yml
index 5df97b9..7f0b692 100644
--- a/roles/ha_vrrp/tasks/main.yml
+++ b/roles/ha_vrrp/tasks/main.yml
@@ -21,7 +21,7 @@
vyos.vyos.vyos_config:
lines: "{{ _ha_vrrp_conntrack_lines }}"
vars:
- _ha_vrrp_conntrack_lines: "{{ lookup('ansible.builtin.template', 'conntrack_sync.j2').splitlines() | select | list }}"
+ _ha_vrrp_conntrack_lines: "{{ (lookup('ansible.builtin.template', 'conntrack_sync.j2') or '').splitlines() | select | list }}"
register: _ha_vrrp_r_conntrack
when:
- _ha_vrrp_cs | length > 0
@@ -32,6 +32,6 @@
vyos_blueprints_rendered: >-
{{ vyos_blueprints_rendered | default([])
+ _ha_vrrp_r_vrrp.rendered | default([])
- + ((lookup('ansible.builtin.template', 'conntrack_sync.j2').splitlines() | select | list)
+ + (((lookup('ansible.builtin.template', 'conntrack_sync.j2') or '').splitlines() | select | list)
if _ha_vrrp_cs | length > 0 else []) }}
when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/ipsec_policy_based/defaults/main.yml b/roles/ipsec_policy_based/defaults/main.yml
new file mode 100644
index 0000000..eeb3961
--- /dev/null
+++ b/roles/ipsec_policy_based/defaults/main.yml
@@ -0,0 +1,19 @@
+---
+ipsec_policy_based_ike_group:
+ name: IKE-GROUP
+ key_exchange: ikev2
+ lifetime: 28800
+ dh_group: 14
+ encryption: aes256
+ hash: sha256
+ close_action: start
+ dead_peer_detection: {action: restart, interval: 10, timeout: 30}
+ipsec_policy_based_esp_group:
+ name: ESP-GROUP
+ lifetime: 3600
+ pfs: dh-group14
+ encryption: aes256
+ hash: sha256
+ipsec_policy_based_default_gateway: ""
+ipsec_policy_based_interfaces: []
+ipsec_policy_based_options: {}
diff --git a/roles/ipsec_policy_based/meta/argument_specs.yml b/roles/ipsec_policy_based/meta/argument_specs.yml
new file mode 100644
index 0000000..48d3341
--- /dev/null
+++ b/roles/ipsec_policy_based/meta/argument_specs.yml
@@ -0,0 +1,145 @@
+---
+argument_specs:
+ main:
+ short_description: Policy-based site-to-site IPsec (docs blueprint "Policy-based Site-to-Site VPN IPsec between VyOS and Cisco")
+ description:
+ - Configures one IKE group, one ESP group, a pre-shared key per peer and
+ site-to-site peers whose tunnels are defined by traffic selectors
+ (local and remote prefixes). Optionally adds a default route towards
+ the WAN gateway.
+ - Uses vyos.vyos resource modules only (vyos_vpn_ipsec, vyos_vpn_ipsec_s2s,
+ vyos_static_routes).
+ - The far end can be any IKE peer; match its proposals and mirror the
+ traffic selectors there.
+ - WAN and LAN addressing belongs to C(vyos.blueprints.base).
+ options:
+ ipsec_policy_based_peers:
+ type: list
+ elements: dict
+ required: true
+ description: Remote peers.
+ options:
+ name:
+ type: str
+ required: true
+ description: Peer name, e.g. C(CISCO).
+ local_address:
+ type: str
+ required: true
+ description: Local WAN address used for IKE.
+ remote_address:
+ type: str
+ required: true
+ description: Peer's WAN address.
+ local_id:
+ type: str
+ description: Local IKE id. Defaults to C(local_address).
+ remote_id:
+ type: str
+ description: Remote IKE id. Defaults to C(remote_address).
+ psk:
+ type: str
+ required: true
+ no_log: true
+ description: Pre-shared key.
+ psk_type:
+ type: str
+ choices: [plaintext, base64, hex]
+ description: Encoding of C(psk). Not set when omitted (VyOS default, plaintext).
+ psk_name:
+ type: str
+ description: Name of the PSK entry. Defaults to C(<name>-PSK).
+ connection_type:
+ type: str
+ choices: [initiate, trap, none]
+ default: initiate
+ description: C(initiate) brings the tunnels up from this side; C(none) only responds.
+ virtual_address:
+ type: str
+ description: Request a virtual IP from the peer, e.g. C(0.0.0.0) for FlexVPN.
+ authentication_ids:
+ type: bool
+ default: true
+ description: Set C(local-id)/C(remote-id) on the peer (the PSK ids are always set).
+ tunnels:
+ type: list
+ elements: dict
+ required: true
+ description: Traffic selectors, one IPsec SA each.
+ options:
+ id:
+ type: int
+ description: Tunnel number. Defaults to the position in the list (1, 2, ...).
+ local_prefix:
+ type: str
+ required: true
+ description: Local network, e.g. C(192.168.0.0/24).
+ remote_prefix:
+ type: str
+ required: true
+ description: Remote network, e.g. C(192.168.10.0/24).
+ protocol:
+ type: str
+ description: Protect only this protocol, e.g. C(gre) for GRE over IPsec.
+ ipsec_policy_based_ike_group:
+ type: dict
+ description:
+ - IKE (phase 1) settings shared by all peers.
+ - Settings without a default here are configured only when given.
+ options:
+ name: {type: str, default: IKE-GROUP, description: Group name.}
+ proposal_id: {type: int, default: 10, description: Proposal number.}
+ key_exchange: {type: str, choices: [ikev1, ikev2], default: ikev2, description: IKE version.}
+ lifetime: {type: int, description: Lifetime in seconds.}
+ dh_group: {type: int, default: 14, description: Diffie-Hellman group.}
+ encryption: {type: str, default: aes256, description: "Encryption, e.g. C(aes128)."}
+ hash: {type: str, default: sha256, description: "Hash, e.g. C(sha1)."}
+ close_action: {type: str, choices: [none, trap, start], description: Action when the peer closes the SA.}
+ dead_peer_detection:
+ type: dict
+ description: DPD settings; only the keys given are configured.
+ options:
+ action: {type: str, choices: [trap, clear, restart], description: DPD action.}
+ interval: {type: int, description: Interval in seconds.}
+ timeout: {type: int, description: Timeout in seconds.}
+ ipsec_policy_based_esp_group:
+ type: dict
+ description:
+ - ESP (phase 2) settings shared by all peers.
+ - Settings without a default here are configured only when given.
+ options:
+ name: {type: str, default: ESP-GROUP, description: Group name.}
+ proposal_id: {type: int, default: 10, description: Proposal number.}
+ mode: {type: str, choices: [tunnel, transport], description: ESP mode.}
+ lifetime: {type: int, description: Lifetime in seconds.}
+ pfs: {type: str, description: PFS group or C(disable).}
+ encryption: {type: str, default: aes256, description: Encryption.}
+ hash: {type: str, default: sha256, description: Hash.}
+ ipsec_policy_based_interfaces:
+ type: list
+ elements: str
+ default: []
+ description: Interfaces IPsec listens on (C(vpn ipsec interface)), e.g. C([eth0]).
+ ipsec_policy_based_options:
+ type: dict
+ default: {}
+ description:
+ - C(vpn ipsec options), passed through as the C(options) of vyos.vyos.vyos_vpn_ipsec.
+ - "For FlexVPN, e.g. C({flexvpn: true, virtual_ip: true, interface: tun1, disable_route_autoinstall: true})."
+ ipsec_policy_based_default_gateway:
+ type: str
+ default: ""
+ description: Adds C(0.0.0.0/0) via this next hop (the WAN gateway).
+ 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 ipsec_policy_based role
+ description: Run with C(tasks_from=verify). Checks the IPsec SA of every tunnel of every peer is up.
+ options:
+ ipsec_policy_based_peers:
+ type: list
+ elements: dict
+ required: true
+ description: Same value as for C(main).
diff --git a/roles/ipsec_policy_based/meta/main.yml b/roles/ipsec_policy_based/meta/main.yml
new file mode 100644
index 0000000..316e8e1
--- /dev/null
+++ b/roles/ipsec_policy_based/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Policy-based site-to-site IPsec with traffic selectors
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, ipsec, vpn]
+dependencies: []
diff --git a/roles/ipsec_policy_based/tasks/main.yml b/roles/ipsec_policy_based/tasks/main.yml
new file mode 100644
index 0000000..c9c7329
--- /dev/null
+++ b/roles/ipsec_policy_based/tasks/main.yml
@@ -0,0 +1,34 @@
+---
+- name: Configure IKE/ESP groups and pre-shared keys
+ vyos.vyos.vyos_vpn_ipsec:
+ config: "{{ lookup('ansible.builtin.template', 'vpn_ipsec.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_policy_based_state }}"
+ register: _ipsec_policy_based_r_ipsec
+ no_log: true
+
+- name: Configure the site-to-site peers and their traffic selectors
+ vyos.vyos.vyos_vpn_ipsec_s2s:
+ config: "{{ lookup('ansible.builtin.template', 'vpn_ipsec_s2s.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_policy_based_state }}"
+ register: _ipsec_policy_based_r_s2s
+
+- name: Add the default route towards the WAN gateway
+ vyos.vyos.vyos_static_routes:
+ config:
+ - address_families:
+ - afi: ipv4
+ routes:
+ - dest: 0.0.0.0/0
+ next_hops:
+ - forward_router_address: "{{ ipsec_policy_based_default_gateway }}"
+ state: "{{ _ipsec_policy_based_state }}"
+ register: _ipsec_policy_based_r_route
+ when: ipsec_policy_based_default_gateway | length > 0
+
+- 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([])
+ + ([_ipsec_policy_based_r_ipsec, _ipsec_policy_based_r_s2s, _ipsec_policy_based_r_route]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/ipsec_policy_based/tasks/verify.yml b/roles/ipsec_policy_based/tasks/verify.yml
new file mode 100644
index 0000000..ec85d8d
--- /dev/null
+++ b/roles/ipsec_policy_based/tasks/verify.yml
@@ -0,0 +1,23 @@
+---
+- name: Read IPsec SAs
+ vyos.vyos.vyos_command:
+ commands:
+ - show vpn ipsec sa
+ register: _ipsec_policy_based_v_sa
+
+- name: The IPsec SA of every tunnel is up
+ ansible.builtin.assert:
+ that: _ipsec_policy_based_v_sa.stdout[0] is search('(?m)^' ~ (item | regex_escape) ~ '\s+up\b')
+ fail_msg: "IPsec SA {{ item }} is not up"
+ quiet: true
+ loop: "{{ _ipsec_policy_based_sa_names }}"
+ vars:
+ # SA names as VyOS shows them: <peer>-tunnel-<id>
+ _ipsec_policy_based_sa_names: >-
+ {%- set out = [] -%}
+ {%- for p in ipsec_policy_based_peers -%}
+ {%- for t in p.tunnels -%}
+ {%- set _ = out.append(p.name ~ '-tunnel-' ~ (t.id | default(loop.index))) -%}
+ {%- endfor -%}
+ {%- endfor -%}
+ {{ out }}
diff --git a/roles/ipsec_policy_based/templates/vpn_ipsec.yml.j2 b/roles/ipsec_policy_based/templates/vpn_ipsec.yml.j2
new file mode 100644
index 0000000..faa824e
--- /dev/null
+++ b/roles/ipsec_policy_based/templates/vpn_ipsec.yml.j2
@@ -0,0 +1,58 @@
+{#- Optional settings are rendered only when given, so a page that leaves
+ something at the VyOS default (lifetime, PFS, DPD, ...) renders as written. -#}
+{% set ike = ipsec_policy_based_ike_group %}
+{% set esp = ipsec_policy_based_esp_group %}
+{% set dpd = ike.dead_peer_detection | default({}, true) %}
+ike_group:
+ - name: {{ ike.name | default('IKE-GROUP') | to_json }}
+ key_exchange: {{ ike.key_exchange | default('ikev2') }}
+{% for k in ['lifetime'] if ike[k] is defined and ike[k] is not none %}
+ {{ k }}: {{ ike[k] | int }}
+{% endfor %}
+{% if ike.close_action is defined and ike.close_action %}
+ close_action: {{ ike.close_action }}
+{% endif %}
+{% if dpd %}
+ dead_peer_detection:
+{% for k in ['action', 'interval', 'timeout'] if dpd[k] is defined and dpd[k] is not none %}
+ {{ k }}: {{ dpd[k] }}
+{% endfor %}
+{% endif %}
+ proposal:
+ - proposal_id: {{ ike.proposal_id | default(10) | int }}
+ dh_group: {{ ike.dh_group | default(14) | int }}
+ encryption: {{ ike.encryption | default('aes256') | to_json }}
+ hash: {{ ike.hash | default('sha256') | to_json }}
+esp_group:
+ - name: {{ esp.name | default('ESP-GROUP') | to_json }}
+{% if esp.mode is defined and esp.mode %}
+ mode: {{ esp.mode }}
+{% endif %}
+{% if esp.lifetime is defined and esp.lifetime is not none %}
+ lifetime: {{ esp.lifetime | int }}
+{% endif %}
+{% if esp.pfs is defined and esp.pfs %}
+ pfs: {{ esp.pfs | to_json }}
+{% endif %}
+ proposal:
+ - proposal_id: {{ esp.proposal_id | default(10) | int }}
+ encryption: {{ esp.encryption | default('aes256') | to_json }}
+ hash: {{ esp.hash | default('sha256') | to_json }}
+authentication:
+ psk:
+{% for p in ipsec_policy_based_peers %}
+ - name: {{ p.psk_name | default(p.name ~ '-PSK') | to_json }}
+ id:
+ - {{ p.local_id | default(p.local_address) | to_json }}
+ - {{ p.remote_id | default(p.remote_address) | to_json }}
+ secret: {{ p.psk | to_json }}
+{% if p.psk_type is defined %}
+ secret_type: {{ p.psk_type }}
+{% endif %}
+{% endfor %}
+{% if ipsec_policy_based_interfaces %}
+interface: {{ ipsec_policy_based_interfaces | to_json }}
+{% endif %}
+{% if ipsec_policy_based_options %}
+options: {{ ipsec_policy_based_options | to_json }}
+{% endif %}
diff --git a/roles/ipsec_policy_based/templates/vpn_ipsec_s2s.yml.j2 b/roles/ipsec_policy_based/templates/vpn_ipsec_s2s.yml.j2
new file mode 100644
index 0000000..cb2277c
--- /dev/null
+++ b/roles/ipsec_policy_based/templates/vpn_ipsec_s2s.yml.j2
@@ -0,0 +1,29 @@
+peer:
+{% for p in ipsec_policy_based_peers %}
+ - name: {{ p.name | to_json }}
+ authentication:
+ mode: pre-shared-secret
+{% if p.authentication_ids | default(true) | bool %}
+ local_id: {{ p.local_id | default(p.local_address) | to_json }}
+ remote_id: {{ p.remote_id | default(p.remote_address) | to_json }}
+{% endif %}
+ connection_type: {{ p.connection_type | default('initiate') }}
+ default_esp_group: {{ ipsec_policy_based_esp_group.name | default('ESP-GROUP') | to_json }}
+ ike_group: {{ ipsec_policy_based_ike_group.name | default('IKE-GROUP') | to_json }}
+ local_address: {{ p.local_address | to_json }}
+ remote_address: [{{ p.remote_address | to_json }}]
+{% if p.virtual_address is defined %}
+ virtual_address: [{{ p.virtual_address | to_json }}]
+{% endif %}
+ tunnel:
+{% for t in p.tunnels %}
+ - tunnel_id: {{ t.id | default(loop.index) | int }}
+ local:
+ prefix: [{{ t.local_prefix | to_json }}]
+ remote:
+ prefix: [{{ t.remote_prefix | to_json }}]
+{% if t.protocol is defined %}
+ protocol: {{ t.protocol | to_json }}
+{% endif %}
+{% endfor %}
+{% endfor %}
diff --git a/roles/ipsec_policy_based/vars/main.yml b/roles/ipsec_policy_based/vars/main.yml
new file mode 100644
index 0000000..f077122
--- /dev/null
+++ b/roles/ipsec_policy_based/vars/main.yml
@@ -0,0 +1,2 @@
+---
+_ipsec_policy_based_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
diff --git a/roles/ipsec_route_based/defaults/main.yml b/roles/ipsec_route_based/defaults/main.yml
new file mode 100644
index 0000000..238cd54
--- /dev/null
+++ b/roles/ipsec_route_based/defaults/main.yml
@@ -0,0 +1,22 @@
+---
+ipsec_route_based_ike_group:
+ name: IKE-GROUP
+ key_exchange: ikev2
+ lifetime: 28800
+ dh_group: 14
+ encryption: aes256
+ hash: sha256
+ close_action: start
+ dead_peer_detection: {action: restart, interval: 10, timeout: 30}
+ipsec_route_based_esp_group:
+ name: ESP-GROUP
+ lifetime: 3600
+ pfs: dh-group14
+ encryption: aes256
+ hash: sha256
+ipsec_route_based_ospf: null
+ipsec_route_based_default_gateway: ""
+ipsec_route_based_interfaces: []
+ipsec_route_based_disable_route_autoinstall: true
+ipsec_route_based_interface_routes: []
+ipsec_route_based_bgp: null
diff --git a/roles/ipsec_route_based/meta/argument_specs.yml b/roles/ipsec_route_based/meta/argument_specs.yml
new file mode 100644
index 0000000..f683376
--- /dev/null
+++ b/roles/ipsec_route_based/meta/argument_specs.yml
@@ -0,0 +1,209 @@
+---
+argument_specs:
+ main:
+ short_description: Route-based site-to-site IPsec over VTI, with OSPF or BGP (Cisco, Palo Alto and Azure docs blueprints)
+ description:
+ - Configures one IKE group, one ESP group, a pre-shared key and a
+ site-to-site peer bound to a VTI per peer, with
+ C(disable-route-autoinstall) as route-based VPNs need. Optionally runs
+ OSPF over the VTIs and adds a default route towards the WAN gateway.
+ - Uses vyos.vyos resource modules only (vyos_vpn_ipsec, vyos_vpn_ipsec_s2s,
+ vyos_interfaces, vyos_l3_interfaces, vyos_ospfv2, vyos_ospf_interfaces,
+ vyos_static_routes).
+ - The far end can be any IKE peer (Cisco, Palo Alto, another VyOS, a cloud
+ gateway); match its proposals in the IKE/ESP group inputs.
+ - WAN and LAN addressing belongs to C(vyos.blueprints.base).
+ options:
+ ipsec_route_based_peers:
+ type: list
+ elements: dict
+ required: true
+ description: Remote peers, one VTI each.
+ options:
+ name:
+ type: str
+ required: true
+ description: Peer name, e.g. C(CISCO).
+ local_address:
+ type: str
+ required: true
+ description: Local WAN address used for IKE.
+ remote_address:
+ type: str
+ required: true
+ description: Peer's WAN address.
+ local_id:
+ type: str
+ description: Local IKE id. Defaults to C(local_address).
+ remote_id:
+ type: str
+ description: Remote IKE id. Defaults to C(remote_address).
+ psk:
+ type: str
+ required: true
+ no_log: true
+ description: Pre-shared key.
+ psk_type:
+ type: str
+ choices: [plaintext, base64, hex]
+ description: Encoding of C(psk). Not set when omitted (VyOS default, plaintext).
+ psk_name:
+ type: str
+ description:
+ - Name of the PSK entry. Defaults to C(<name>-PSK).
+ - Peers with the same C(psk_name) share one entry carrying all their ids (they must use the same C(psk)).
+ connection_type:
+ type: str
+ choices: [initiate, trap, none]
+ default: initiate
+ description:
+ - C(initiate) brings the tunnel up from this side (recommended by
+ the page when the far end only initiates on traffic); C(none)
+ only responds.
+ description:
+ type: str
+ description: Peer description.
+ ikev2_reauth:
+ type: str
+ choices: ['yes', 'no', inherit]
+ description: Peer-level IKEv2 re-authentication.
+ esp_group_on_vti:
+ type: bool
+ default: false
+ description: Bind the ESP group on the VTI (C(vti esp-group)) instead of C(default-esp-group), as Azure and GCP pages do.
+ vti:
+ type: dict
+ required: true
+ description: Tunnel interface for this peer.
+ options:
+ interface:
+ type: str
+ required: true
+ description: VTI name, e.g. C(vti1).
+ address:
+ type: str
+ required: true
+ description: Tunnel address in CIDR notation.
+ mtu:
+ type: int
+ description: VTI MTU, e.g. C(1438).
+ description:
+ type: str
+ description: VTI description.
+ adjust_mss:
+ type: str
+ description: TCP MSS clamping, e.g. C(1350) (configured with vyos_config; vyos_interfaces has no option for it).
+ ipsec_route_based_ike_group:
+ type: dict
+ description:
+ - IKE (phase 1) settings shared by all peers.
+ - Settings without a default here are configured only when given.
+ options:
+ name: {type: str, default: IKE-GROUP, description: Group name.}
+ proposal_id: {type: int, default: 10, description: Proposal number.}
+ key_exchange: {type: str, choices: [ikev1, ikev2], default: ikev2, description: IKE version.}
+ ikev2_reauth: {type: bool, default: false, description: Re-authenticate on IKEv2 rekey.}
+ lifetime: {type: int, description: Lifetime in seconds.}
+ dh_group: {type: int, default: 14, description: Diffie-Hellman group.}
+ encryption: {type: str, default: aes256, description: "Encryption, e.g. C(aes128)."}
+ hash: {type: str, default: sha256, description: "Hash, e.g. C(sha1)."}
+ close_action: {type: str, choices: [none, trap, start], description: Action when the peer closes the SA.}
+ dead_peer_detection:
+ type: dict
+ description: DPD settings; only the keys given are configured.
+ options:
+ action: {type: str, choices: [trap, clear, restart], description: DPD action.}
+ interval: {type: int, description: Interval in seconds.}
+ timeout: {type: int, description: Timeout in seconds.}
+ ipsec_route_based_esp_group:
+ type: dict
+ description:
+ - ESP (phase 2) settings shared by all peers.
+ - Settings without a default here are configured only when given.
+ options:
+ name: {type: str, default: ESP-GROUP, description: Group name.}
+ proposal_id: {type: int, default: 10, description: Proposal number.}
+ mode: {type: str, choices: [tunnel, transport], description: ESP mode.}
+ lifetime: {type: int, description: Lifetime in seconds.}
+ pfs: {type: str, description: PFS group or C(disable).}
+ encryption: {type: str, default: aes256, description: Encryption.}
+ hash: {type: str, default: sha256, description: Hash.}
+ ipsec_route_based_ospf:
+ type: dict
+ description: OSPF over the VTIs. Omit to configure routing yourself.
+ options:
+ router_id: {type: str, required: true, description: OSPF router-id.}
+ area: {type: str, default: "0", description: OSPF area.}
+ networks:
+ type: list
+ elements: str
+ required: true
+ description: Networks to advertise, including the VTI network(s), as on the docs page.
+ passive_interfaces:
+ type: list
+ elements: str
+ default: []
+ description: LAN interfaces that should not form adjacencies.
+ ipsec_route_based_interfaces:
+ type: list
+ elements: str
+ default: []
+ description: Interfaces IPsec listens on (C(vpn ipsec interface)), e.g. C([eth0]).
+ ipsec_route_based_disable_route_autoinstall:
+ type: bool
+ default: true
+ description: Set C(vpn ipsec options disable-route-autoinstall) (most route-based setups need it).
+ ipsec_route_based_interface_routes:
+ type: list
+ elements: dict
+ default: []
+ description: Static routes out of a VTI, e.g. to the peer's BGP listener.
+ options:
+ dest: {type: str, required: true, description: "Destination prefix, e.g. C(10.0.0.4/32)."}
+ interface: {type: str, required: true, description: "VTI, e.g. C(vti1)."}
+ ipsec_route_based_bgp:
+ type: dict
+ description: eBGP over the VTIs. Omit to configure routing yourself.
+ options:
+ asn: {type: int, required: true, description: Local AS number.}
+ router_id: {type: str, description: BGP router-id.}
+ networks: {type: list, elements: str, default: [], description: IPv4 prefixes to announce.}
+ neighbors:
+ type: list
+ elements: dict
+ required: true
+ description: BGP neighbours reached through the tunnels.
+ options:
+ address: {type: str, required: true, description: Neighbour address.}
+ remote_as: {type: int, required: true, description: Neighbour AS number.}
+ holdtime: {type: int, description: Hold time in seconds.}
+ keepalive: {type: int, description: Keepalive in seconds.}
+ disable_connected_check: {type: bool, default: true, description: Needed when the neighbour is not on the VTI subnet (cloud BGP listeners).}
+ ebgp_multihop: {type: int, description: eBGP multihop TTL.}
+ update_source: {type: str, description: Source address or interface.}
+ soft_reconfiguration_inbound: {type: bool, default: false, description: Keep received routes for soft reconfiguration.}
+ ipsec_route_based_default_gateway:
+ type: str
+ default: ""
+ description: Adds C(0.0.0.0/0) via this next hop (the WAN gateway).
+ 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 ipsec_route_based role
+ description:
+ - Run with C(tasks_from=verify). Checks the IPsec SA of every peer is up and,
+ with OSPF, a Full adjacency on every VTI or, with BGP, every session established.
+ options:
+ ipsec_route_based_peers:
+ type: list
+ elements: dict
+ required: true
+ description: Same value as for C(main).
+ ipsec_route_based_ospf:
+ type: dict
+ description: Same value as for C(main).
+ ipsec_route_based_bgp:
+ type: dict
+ description: Same value as for C(main).
diff --git a/roles/ipsec_route_based/meta/main.yml b/roles/ipsec_route_based/meta/main.yml
new file mode 100644
index 0000000..95831bc
--- /dev/null
+++ b/roles/ipsec_route_based/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Route-based site-to-site IPsec over VTI, with optional OSPF inside the tunnel
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, ipsec, vpn]
+dependencies: []
diff --git a/roles/ipsec_route_based/tasks/main.yml b/roles/ipsec_route_based/tasks/main.yml
new file mode 100644
index 0000000..a3b4c16
--- /dev/null
+++ b/roles/ipsec_route_based/tasks/main.yml
@@ -0,0 +1,113 @@
+---
+- name: Peers sharing a PSK entry use the same key
+ ansible.builtin.assert:
+ that: >-
+ ipsec_route_based_peers | selectattr('psk_name', 'defined') | selectattr('psk_name', 'equalto', _ipsec_route_based_psk_name)
+ | map(attribute='psk') | unique | length == 1
+ fail_msg: "peers with psk_name {{ _ipsec_route_based_psk_name }} have different psk values"
+ quiet: true
+ loop: "{{ ipsec_route_based_peers | selectattr('psk_name', 'defined') | map(attribute='psk_name') | unique | list }}"
+ loop_control:
+ loop_var: _ipsec_route_based_psk_name
+ no_log: true
+
+- name: Create the VTIs
+ vyos.vyos.vyos_interfaces:
+ config: "{{ lookup('ansible.builtin.template', 'vti.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_vti
+
+- name: Clamp TCP MSS on the VTIs
+ # vyos_interfaces has no option for ip adjust-mss.
+ vyos.vyos.vyos_config:
+ lines: "{{ _ipsec_route_based_cli_lines }}"
+ register: _ipsec_route_based_r_cli
+ when:
+ - _ipsec_route_based_cli_lines | length > 0
+ - not (vyos_blueprints_render_only | default(false) | bool)
+
+- name: Address the VTIs
+ vyos.vyos.vyos_l3_interfaces:
+ config: "{{ lookup('ansible.builtin.template', 'vti_l3.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_vti_l3
+
+- name: Configure IKE/ESP groups, pre-shared keys and options
+ vyos.vyos.vyos_vpn_ipsec:
+ config: "{{ lookup('ansible.builtin.template', 'vpn_ipsec.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_ipsec
+ no_log: true
+
+- name: Configure the site-to-site peers
+ vyos.vyos.vyos_vpn_ipsec_s2s:
+ config: "{{ lookup('ansible.builtin.template', 'vpn_ipsec_s2s.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_s2s
+
+- name: Add the default route towards the WAN gateway
+ vyos.vyos.vyos_static_routes:
+ config:
+ - address_families:
+ - afi: ipv4
+ routes:
+ - dest: 0.0.0.0/0
+ next_hops:
+ - forward_router_address: "{{ ipsec_route_based_default_gateway }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_route
+ when: ipsec_route_based_default_gateway | length > 0
+
+- name: Add interface routes through the VTIs (e.g. to the peer's BGP listener)
+ vyos.vyos.vyos_static_routes:
+ config:
+ - address_families:
+ - afi: ipv4
+ routes: "{{ lookup('ansible.builtin.template', 'interface_routes.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_ifroutes
+ when: ipsec_route_based_interface_routes | length > 0
+
+- name: Run BGP over the VTIs
+ vyos.vyos.vyos_bgp_global:
+ config: "{{ lookup('ansible.builtin.template', 'bgp_global.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_bgp
+ when: _ipsec_route_based_bgp | length > 0
+
+- name: Set BGP address-family options (networks, soft-reconfiguration)
+ vyos.vyos.vyos_bgp_address_family:
+ config: "{{ _ipsec_route_based_bgp_af }}"
+ state: "{{ _ipsec_route_based_state }}"
+ vars:
+ _ipsec_route_based_bgp_af: "{{ lookup('ansible.builtin.template', 'bgp_address_family.yml.j2') | from_yaml }}"
+ register: _ipsec_route_based_r_bgp_af
+ when:
+ - _ipsec_route_based_bgp | length > 0
+ - _ipsec_route_based_bgp_af | length > 1
+
+- name: Run OSPF over the VTIs
+ vyos.vyos.vyos_ospfv2:
+ config: "{{ lookup('ansible.builtin.template', 'ospfv2.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_ospf
+ when: _ipsec_route_based_ospf | length > 0
+
+- name: Set OSPF interface options (point-to-point VTIs, passive LANs)
+ vyos.vyos.vyos_ospf_interfaces:
+ config: "{{ lookup('ansible.builtin.template', 'ospf_interfaces.yml.j2') | from_yaml }}"
+ state: "{{ _ipsec_route_based_state }}"
+ register: _ipsec_route_based_r_ospf_if
+ when: _ipsec_route_based_ospf | length > 0
+
+- 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([])
+ + (_ipsec_route_based_r_vti.rendered | default([]))
+ + _ipsec_route_based_cli_lines
+ + ([_ipsec_route_based_r_vti_l3, _ipsec_route_based_r_ipsec, _ipsec_route_based_r_s2s,
+ _ipsec_route_based_r_route, _ipsec_route_based_r_ifroutes, _ipsec_route_based_r_bgp,
+ _ipsec_route_based_r_bgp_af, _ipsec_route_based_r_ospf, _ipsec_route_based_r_ospf_if]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten | unique) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/ipsec_route_based/tasks/verify.yml b/roles/ipsec_route_based/tasks/verify.yml
new file mode 100644
index 0000000..42edd24
--- /dev/null
+++ b/roles/ipsec_route_based/tasks/verify.yml
@@ -0,0 +1,52 @@
+---
+- name: Read IPsec SAs
+ vyos.vyos.vyos_command:
+ commands:
+ - show vpn ipsec sa
+ register: _ipsec_route_based_v_sa
+
+- name: The IPsec SA of every peer is up
+ ansible.builtin.assert:
+ that: _ipsec_route_based_v_sa.stdout[0] is search('(?m)^' ~ (item.name | regex_escape) ~ '-vti\s+up\b')
+ fail_msg: "IPsec SA {{ item.name }}-vti is not up"
+ quiet: true
+ loop: "{{ ipsec_route_based_peers }}"
+ loop_control:
+ label: "{{ item.name }}"
+
+- name: Read OSPF neighbours
+ vyos.vyos.vyos_command:
+ commands:
+ - show ip ospf neighbor
+ register: _ipsec_route_based_v_ospf
+ when: ipsec_route_based_ospf | default({}, true) | length > 0
+
+- name: A Full OSPF adjacency on every VTI
+ ansible.builtin.assert:
+ that: _ipsec_route_based_v_ospf.stdout[0] is search('Full.*\s' ~ (item.vti.interface | regex_escape) ~ ':')
+ fail_msg: "no Full OSPF adjacency on {{ item.vti.interface }}"
+ quiet: true
+ loop: "{{ ipsec_route_based_peers }}"
+ loop_control:
+ label: "{{ item.vti.interface }}"
+ when: ipsec_route_based_ospf | default({}, true) | length > 0
+
+- name: Read BGP neighbours
+ vyos.vyos.vyos_command:
+ commands:
+ - show bgp summary
+ register: _ipsec_route_based_v_bgp
+ when: ipsec_route_based_bgp | default({}, true) | length > 0
+
+- name: Every BGP session is established
+ # An established neighbour shows its Up/Down time followed by a prefix count.
+ ansible.builtin.assert:
+ that: >-
+ _ipsec_route_based_v_bgp.stdout[0] is search('(?m)^' ~ (item.address | regex_escape)
+ ~ '\s.*\s(\d{2}:\d{2}:\d{2}|\d+[dwh]\S*)\s+\d+\b')
+ fail_msg: "BGP session to {{ item.address }} is not established"
+ quiet: true
+ loop: "{{ (ipsec_route_based_bgp | default({}, true)).neighbors | default([]) }}"
+ loop_control:
+ label: "{{ item.address }}"
+ when: ipsec_route_based_bgp | default({}, true) | length > 0
diff --git a/roles/ipsec_route_based/templates/bgp_address_family.yml.j2 b/roles/ipsec_route_based/templates/bgp_address_family.yml.j2
new file mode 100644
index 0000000..97e88cd
--- /dev/null
+++ b/roles/ipsec_route_based/templates/bgp_address_family.yml.j2
@@ -0,0 +1,20 @@
+{% set b = _ipsec_route_based_bgp %}
+as_number: {{ b.asn | int }}
+{% if b.networks | default([]) %}
+address_family:
+ - afi: ipv4
+ networks:
+{% for n in b.networks %}
+ - prefix: {{ n | to_json }}
+{% endfor %}
+{% endif %}
+{% set soft = b.neighbors | selectattr('soft_reconfiguration_inbound', 'defined') | selectattr('soft_reconfiguration_inbound') | list %}
+{% if soft %}
+neighbors:
+{% for n in soft %}
+ - neighbor_address: {{ n.address | to_json }}
+ address_family:
+ - afi: ipv4
+ soft_reconfiguration: true
+{% endfor %}
+{% endif %}
diff --git a/roles/ipsec_route_based/templates/bgp_global.yml.j2 b/roles/ipsec_route_based/templates/bgp_global.yml.j2
new file mode 100644
index 0000000..f4bcf33
--- /dev/null
+++ b/roles/ipsec_route_based/templates/bgp_global.yml.j2
@@ -0,0 +1,29 @@
+{% set b = _ipsec_route_based_bgp %}
+as_number: {{ b.asn | int }}
+{% if b.router_id is defined %}
+bgp_params:
+ router_id: {{ b.router_id | to_json }}
+{% endif %}
+neighbor:
+{% for n in b.neighbors %}
+ - address: {{ n.address | to_json }}
+ remote_as: {{ n.remote_as | int }}
+{% if n.holdtime is defined or n.keepalive is defined %}
+ timers:
+{% if n.holdtime is defined %}
+ holdtime: {{ n.holdtime | int }}
+{% endif %}
+{% if n.keepalive is defined %}
+ keepalive: {{ n.keepalive | int }}
+{% endif %}
+{% endif %}
+{% if n.disable_connected_check | default(true) | bool %}
+ disable_connected_check: true
+{% endif %}
+{% if n.ebgp_multihop is defined %}
+ ebgp_multihop: {{ n.ebgp_multihop | int }}
+{% endif %}
+{% if n.update_source is defined %}
+ update_source: {{ n.update_source | to_json }}
+{% endif %}
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/interface_routes.yml.j2 b/roles/ipsec_route_based/templates/interface_routes.yml.j2
new file mode 100644
index 0000000..8039e19
--- /dev/null
+++ b/roles/ipsec_route_based/templates/interface_routes.yml.j2
@@ -0,0 +1,5 @@
+{% for r in ipsec_route_based_interface_routes %}
+- dest: {{ r.dest | to_json }}
+ next_hops:
+ - interface: {{ r.interface | to_json }}
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/ospf_interfaces.yml.j2 b/roles/ipsec_route_based/templates/ospf_interfaces.yml.j2
new file mode 100644
index 0000000..09452ea
--- /dev/null
+++ b/roles/ipsec_route_based/templates/ospf_interfaces.yml.j2
@@ -0,0 +1,12 @@
+{% for i in _ipsec_route_based_ospf.passive_interfaces | default([]) %}
+- name: {{ i }}
+ address_family:
+ - afi: ipv4
+ passive: true
+{% endfor %}
+{% for p in ipsec_route_based_peers %}
+- name: {{ p.vti.interface }}
+ address_family:
+ - afi: ipv4
+ network: point-to-point
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/ospfv2.yml.j2 b/roles/ipsec_route_based/templates/ospfv2.yml.j2
new file mode 100644
index 0000000..93ee2ff
--- /dev/null
+++ b/roles/ipsec_route_based/templates/ospfv2.yml.j2
@@ -0,0 +1,9 @@
+{% set o = _ipsec_route_based_ospf %}
+parameters:
+ router_id: {{ o.router_id | to_json }}
+areas:
+ - area_id: {{ o.area | default('0') | string | to_json }}
+ network:
+{% for n in o.networks %}
+ - address: {{ n | to_json }}
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/vpn_ipsec.yml.j2 b/roles/ipsec_route_based/templates/vpn_ipsec.yml.j2
new file mode 100644
index 0000000..a9eab25
--- /dev/null
+++ b/roles/ipsec_route_based/templates/vpn_ipsec.yml.j2
@@ -0,0 +1,51 @@
+{#- Optional settings are rendered only when given. -#}
+{% set ike = ipsec_route_based_ike_group %}
+{% set esp = ipsec_route_based_esp_group %}
+{% set dpd = ike.dead_peer_detection | default({}, true) %}
+ike_group:
+ - name: {{ ike.name | default('IKE-GROUP') | to_json }}
+ key_exchange: {{ ike.key_exchange | default('ikev2') }}
+{% if ike.lifetime is defined and ike.lifetime is not none %}
+ lifetime: {{ ike.lifetime | int }}
+{% endif %}
+{% if ike.close_action is defined and ike.close_action %}
+ close_action: {{ ike.close_action }}
+{% endif %}
+{% if ike.ikev2_reauth | default(false) | bool %}
+ ikev2_reauth: true
+{% endif %}
+{% if dpd %}
+ dead_peer_detection:
+{% for k in ['action', 'interval', 'timeout'] if dpd[k] is defined and dpd[k] is not none %}
+ {{ k }}: {{ dpd[k] }}
+{% endfor %}
+{% endif %}
+ proposal:
+ - proposal_id: {{ ike.proposal_id | default(10) | int }}
+ dh_group: {{ ike.dh_group | default(14) | int }}
+ encryption: {{ ike.encryption | default('aes256') | to_json }}
+ hash: {{ ike.hash | default('sha256') | to_json }}
+esp_group:
+ - name: {{ esp.name | default('ESP-GROUP') | to_json }}
+{% if esp.mode is defined and esp.mode %}
+ mode: {{ esp.mode }}
+{% endif %}
+{% if esp.lifetime is defined and esp.lifetime is not none %}
+ lifetime: {{ esp.lifetime | int }}
+{% endif %}
+{% if esp.pfs is defined and esp.pfs %}
+ pfs: {{ esp.pfs | to_json }}
+{% endif %}
+ proposal:
+ - proposal_id: {{ esp.proposal_id | default(10) | int }}
+ encryption: {{ esp.encryption | default('aes256') | to_json }}
+ hash: {{ esp.hash | default('sha256') | to_json }}
+authentication:
+ psk: {{ _ipsec_route_based_psks | to_json }}
+{% if ipsec_route_based_interfaces %}
+interface: {{ ipsec_route_based_interfaces | to_json }}
+{% endif %}
+{% if ipsec_route_based_disable_route_autoinstall | bool %}
+options:
+ disable_route_autoinstall: true
+{% endif %}
diff --git a/roles/ipsec_route_based/templates/vpn_ipsec_s2s.yml.j2 b/roles/ipsec_route_based/templates/vpn_ipsec_s2s.yml.j2
new file mode 100644
index 0000000..2dba193
--- /dev/null
+++ b/roles/ipsec_route_based/templates/vpn_ipsec_s2s.yml.j2
@@ -0,0 +1,27 @@
+{% set esp_name = ipsec_route_based_esp_group.name | default('ESP-GROUP') %}
+peer:
+{% for p in ipsec_route_based_peers %}
+ - name: {{ p.name | to_json }}
+{% if p.description is defined %}
+ description: {{ p.description | to_json }}
+{% endif %}
+ authentication:
+ mode: pre-shared-secret
+ local_id: {{ p.local_id | default(p.local_address) | to_json }}
+ remote_id: {{ p.remote_id | default(p.remote_address) | to_json }}
+ connection_type: {{ p.connection_type | default('initiate') }}
+{% if not (p.esp_group_on_vti | default(false) | bool) %}
+ default_esp_group: {{ esp_name | to_json }}
+{% endif %}
+ ike_group: {{ ipsec_route_based_ike_group.name | default('IKE-GROUP') | to_json }}
+{% if p.ikev2_reauth is defined %}
+ ikev2_reauth: {{ p.ikev2_reauth | to_json }}
+{% endif %}
+ local_address: {{ p.local_address | to_json }}
+ remote_address: [{{ p.remote_address | to_json }}]
+ vti:
+ bind: {{ p.vti.interface | to_json }}
+{% if p.esp_group_on_vti | default(false) | bool %}
+ esp_group: {{ esp_name | to_json }}
+{% endif %}
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/vti.yml.j2 b/roles/ipsec_route_based/templates/vti.yml.j2
new file mode 100644
index 0000000..2047055
--- /dev/null
+++ b/roles/ipsec_route_based/templates/vti.yml.j2
@@ -0,0 +1,9 @@
+{% for p in ipsec_route_based_peers %}
+- name: {{ p.vti.interface }}
+{% if p.vti.mtu is defined %}
+ mtu: {{ p.vti.mtu | int }}
+{% endif %}
+{% if p.vti.description is defined %}
+ description: {{ p.vti.description | to_json }}
+{% endif %}
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/vti_cli.j2 b/roles/ipsec_route_based/templates/vti_cli.j2
new file mode 100644
index 0000000..8dddeb0
--- /dev/null
+++ b/roles/ipsec_route_based/templates/vti_cli.j2
@@ -0,0 +1,4 @@
+{# MSS clamping on VTIs: vyos_interfaces has no option for it. #}
+{% for p in ipsec_route_based_peers if p.vti.adjust_mss is defined %}
+set interfaces vti {{ p.vti.interface }} ip adjust-mss '{{ p.vti.adjust_mss }}'
+{% endfor %}
diff --git a/roles/ipsec_route_based/templates/vti_l3.yml.j2 b/roles/ipsec_route_based/templates/vti_l3.yml.j2
new file mode 100644
index 0000000..a54b5de
--- /dev/null
+++ b/roles/ipsec_route_based/templates/vti_l3.yml.j2
@@ -0,0 +1,5 @@
+{% for p in ipsec_route_based_peers %}
+- name: {{ p.vti.interface }}
+ ipv4:
+ - address: {{ p.vti.address | to_json }}
+{% endfor %}
diff --git a/roles/ipsec_route_based/vars/main.yml b/roles/ipsec_route_based/vars/main.yml
new file mode 100644
index 0000000..2db5c62
--- /dev/null
+++ b/roles/ipsec_route_based/vars/main.yml
@@ -0,0 +1,23 @@
+---
+_ipsec_route_based_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_ipsec_route_based_ospf: "{{ ipsec_route_based_ospf | default({}, true) }}"
+_ipsec_route_based_bgp: "{{ ipsec_route_based_bgp | default({}, true) }}"
+_ipsec_route_based_cli_lines: "{{ (lookup('ansible.builtin.template', 'vti_cli.j2') or '').splitlines() | select | list }}"
+# One PSK entry per psk_name; peers sharing a psk_name contribute all their ids.
+_ipsec_route_based_psks: >-
+ {%- set out = [] -%}
+ {%- set names = [] -%}
+ {%- for p in ipsec_route_based_peers -%}
+ {%- set n = p.psk_name | default(p.name ~ '-PSK') -%}
+ {%- set ids = [p.local_id | default(p.local_address), p.remote_id | default(p.remote_address)] -%}
+ {%- if n in names -%}
+ {%- set e = out[names.index(n)] -%}
+ {%- for i in ids if i not in e.id -%}{%- set _ = e.id.append(i) -%}{%- endfor -%}
+ {%- else -%}
+ {%- set e = {'name': n, 'id': ids | unique | list, 'secret': p.psk} -%}
+ {%- if p.psk_type is defined -%}{%- set _ = e.update({'secret_type': p.psk_type}) -%}{%- endif -%}
+ {%- set _ = names.append(n) -%}
+ {%- set _ = out.append(e) -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {{ out }}
diff --git a/roles/nat/defaults/main.yml b/roles/nat/defaults/main.yml
new file mode 100644
index 0000000..6ba6f0a
--- /dev/null
+++ b/roles/nat/defaults/main.yml
@@ -0,0 +1,3 @@
+---
+nat_source_rules: []
+nat_destination_rules: []
diff --git a/roles/nat/meta/argument_specs.yml b/roles/nat/meta/argument_specs.yml
new file mode 100644
index 0000000..43c260e
--- /dev/null
+++ b/roles/nat/meta/argument_specs.yml
@@ -0,0 +1,33 @@
+---
+argument_specs:
+ main:
+ short_description: Source and destination NAT
+ description:
+ - Configures source NAT (masquerade, SNAT, exclusions such as
+ traffic into a VPN) and destination NAT (port forwarding).
+ - Rules use the rule format of vyos.vyos.vyos_nat (id, description,
+ protocol, exclude, outbound_interface / inbound_interface, source,
+ destination, translation, ...), passed through unchanged, so every
+ option of the module is available.
+ - For a simple small-office edge, C(vyos.blueprints.edge_nat) is shorter.
+ options:
+ nat_source_rules:
+ type: list
+ elements: dict
+ default: []
+ description: "Source NAT rules in vyos_nat format, e.g. C({id: 20, outbound_interface: {name: eth0}, translation: {address: masquerade}})."
+ nat_destination_rules:
+ type: list
+ elements: dict
+ default: []
+ description: Destination NAT rules in vyos_nat format.
+ 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 nat role
+ description: Run with C(tasks_from=verify). Checks every rule is installed.
+ options:
+ nat_source_rules: {type: list, elements: dict, default: [], description: Same value as for C(main).}
+ nat_destination_rules: {type: list, elements: dict, default: [], description: Same value as for C(main).}
diff --git a/roles/nat/meta/main.yml b/roles/nat/meta/main.yml
new file mode 100644
index 0000000..682f983
--- /dev/null
+++ b/roles/nat/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Source and destination NAT rules
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, nat]
+dependencies: []
diff --git a/roles/nat/tasks/main.yml b/roles/nat/tasks/main.yml
new file mode 100644
index 0000000..6f5007b
--- /dev/null
+++ b/roles/nat/tasks/main.yml
@@ -0,0 +1,15 @@
+---
+- name: Configure source and destination NAT
+ vyos.vyos.vyos_nat:
+ config:
+ nat: >-
+ {{ ({'source': {'rule': nat_source_rules}} if nat_source_rules else {})
+ | combine({'destination': {'rule': nat_destination_rules}} if nat_destination_rules else {}) }}
+ state: "{{ _nat_state }}"
+ register: _nat_r
+ when: (nat_source_rules | length > 0) or (nat_destination_rules | length > 0)
+
+- 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([]) + (_nat_r.rendered | default([])) }}"
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/nat/tasks/verify.yml b/roles/nat/tasks/verify.yml
new file mode 100644
index 0000000..a100d3a
--- /dev/null
+++ b/roles/nat/tasks/verify.yml
@@ -0,0 +1,25 @@
+---
+- name: Read NAT rules
+ vyos.vyos.vyos_command:
+ commands:
+ - show nat source rules
+ - show nat destination rules
+ register: _nat_v
+
+- name: Every source NAT rule is installed
+ ansible.builtin.assert:
+ that: _nat_v.stdout[0] is search('(?m)^\s*' ~ item.id ~ '\b')
+ fail_msg: "source NAT rule {{ item.id }} is not installed"
+ quiet: true
+ loop: "{{ nat_source_rules }}"
+ loop_control:
+ label: "{{ item.id }}"
+
+- name: Every destination NAT rule is installed
+ ansible.builtin.assert:
+ that: _nat_v.stdout[1] is search('(?m)^\s*' ~ item.id ~ '\b')
+ fail_msg: "destination NAT rule {{ item.id }} is not installed"
+ quiet: true
+ loop: "{{ nat_destination_rules }}"
+ loop_control:
+ label: "{{ item.id }}"
diff --git a/roles/nat/vars/main.yml b/roles/nat/vars/main.yml
new file mode 100644
index 0000000..a354487
--- /dev/null
+++ b/roles/nat/vars/main.yml
@@ -0,0 +1,2 @@
+---
+_nat_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
diff --git a/roles/ospf_unnumbered/defaults/main.yml b/roles/ospf_unnumbered/defaults/main.yml
new file mode 100644
index 0000000..b1ed553
--- /dev/null
+++ b/roles/ospf_unnumbered/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+ospf_unnumbered_area: 0.0.0.0
+ospf_unnumbered_md5_key: ""
+ospf_unnumbered_md5_key_id: 1
+ospf_unnumbered_redistribute: [connected]
+ospf_unnumbered_group: ""
diff --git a/roles/ospf_unnumbered/meta/argument_specs.yml b/roles/ospf_unnumbered/meta/argument_specs.yml
new file mode 100644
index 0000000..c5c5407
--- /dev/null
+++ b/roles/ospf_unnumbered/meta/argument_specs.yml
@@ -0,0 +1,68 @@
+---
+argument_specs:
+ main:
+ short_description: OSPF unnumbered with ECMP (docs blueprint "OSPF unnumbered with ECMP")
+ description:
+ - Puts the router-id as a /32 on the loopback and on every listed link,
+ runs OSPF point-to-point over those links and redistributes connected
+ routes. Parallel links to the same neighbour give ECMP.
+ - Works for any number of routers; each router lists only its own links.
+ - LAN addressing (C(eth0) on the docs page) belongs to C(vyos.blueprints.base).
+ options:
+ ospf_unnumbered_router_id:
+ type: str
+ required: true
+ description: OSPF router-id; also used as the /32 address on C(lo) and on each link.
+ ospf_unnumbered_interfaces:
+ type: list
+ elements: str
+ required: true
+ description: Unnumbered point-to-point links, e.g. C([eth1, eth2]).
+ ospf_unnumbered_area:
+ type: str
+ default: 0.0.0.0
+ description: OSPF area.
+ ospf_unnumbered_md5_key:
+ type: str
+ default: ""
+ no_log: true
+ description: MD5 key for OSPF authentication. Empty disables authentication.
+ ospf_unnumbered_md5_key_id:
+ type: int
+ default: 1
+ description: MD5 key id.
+ ospf_unnumbered_redistribute:
+ type: list
+ elements: str
+ default: [connected]
+ description: Route types redistributed into OSPF.
+ ospf_unnumbered_group:
+ type: str
+ default: ""
+ description: Inventory group of all routers in the fabric; used only by C(verify).
+ 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 ospf_unnumbered role
+ description:
+ - Run with C(tasks_from=verify) against all routers in the same play.
+ - Checks one Full adjacency per listed link and, with
+ C(ospf_unnumbered_group), an OSPF route to every other router-id.
+ When the group has exactly two routers, also checks one ECMP next
+ hop per link.
+ options:
+ ospf_unnumbered_router_id:
+ type: str
+ required: true
+ description: Same value as for C(main).
+ ospf_unnumbered_interfaces:
+ type: list
+ elements: str
+ required: true
+ description: Same value as for C(main).
+ ospf_unnumbered_group:
+ type: str
+ default: ""
+ description: Same value as for C(main).
diff --git a/roles/ospf_unnumbered/meta/main.yml b/roles/ospf_unnumbered/meta/main.yml
new file mode 100644
index 0000000..c24d6e2
--- /dev/null
+++ b/roles/ospf_unnumbered/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: OSPF over unnumbered point-to-point links with ECMP
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, ospf]
+dependencies: []
diff --git a/roles/ospf_unnumbered/tasks/main.yml b/roles/ospf_unnumbered/tasks/main.yml
new file mode 100644
index 0000000..1ac2c5c
--- /dev/null
+++ b/roles/ospf_unnumbered/tasks/main.yml
@@ -0,0 +1,28 @@
+---
+- name: Put the router-id on the loopback and on every unnumbered link
+ vyos.vyos.vyos_l3_interfaces:
+ config: "{{ lookup('ansible.builtin.template', 'l3_interfaces.yml.j2') | from_yaml }}"
+ state: "{{ _ospf_unnumbered_state }}"
+ register: _ospf_unnumbered_r_l3
+
+- name: Run OSPF point-to-point on the links
+ vyos.vyos.vyos_ospf_interfaces:
+ config: "{{ lookup('ansible.builtin.template', 'ospf_interfaces.yml.j2') | from_yaml }}"
+ state: "{{ _ospf_unnumbered_state }}"
+ register: _ospf_unnumbered_r_ifaces
+ no_log: "{{ ospf_unnumbered_md5_key | length > 0 }}"
+
+- name: Configure the OSPF process
+ vyos.vyos.vyos_ospfv2:
+ config: "{{ lookup('ansible.builtin.template', 'ospfv2.yml.j2') | from_yaml }}"
+ state: "{{ _ospf_unnumbered_state }}"
+ register: _ospf_unnumbered_r_ospf
+
+- 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([])
+ + ([_ospf_unnumbered_r_l3, _ospf_unnumbered_r_ifaces, _ospf_unnumbered_r_ospf]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten
+ | map('regex_replace', '(md5-key) \S+', "\1 '********'")) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/ospf_unnumbered/tasks/verify.yml b/roles/ospf_unnumbered/tasks/verify.yml
new file mode 100644
index 0000000..50b96fd
--- /dev/null
+++ b/roles/ospf_unnumbered/tasks/verify.yml
@@ -0,0 +1,42 @@
+---
+- name: Read OSPF neighbours
+ vyos.vyos.vyos_command:
+ commands:
+ - show ip ospf neighbor
+ register: _ospf_unnumbered_v_nbr
+
+- name: One Full adjacency per unnumbered link
+ ansible.builtin.assert:
+ that: _ospf_unnumbered_v_nbr.stdout[0] is search('Full.*\s' ~ item ~ ':')
+ fail_msg: "no Full OSPF adjacency on {{ item }}"
+ quiet: true
+ loop: "{{ ospf_unnumbered_interfaces }}"
+
+- name: Read routes to the other routers
+ vyos.vyos.vyos_command:
+ commands: "{{ _ospf_unnumbered_peers | map('regex_replace', '^(.*)$', 'show ip route \\1/32') | list }}"
+ vars:
+ _ospf_unnumbered_peers: >-
+ {{ groups[ospf_unnumbered_group] | difference([inventory_hostname])
+ | map('extract', hostvars, 'ospf_unnumbered_router_id') | list }}
+ register: _ospf_unnumbered_v_routes
+ when: ospf_unnumbered_group | length > 0
+
+- name: Every other router-id is reached via OSPF
+ ansible.builtin.assert:
+ that: item is search('Known via "ospf"')
+ fail_msg: "{{ item | regex_search('Routing entry for \\S+') | default('a peer', true) }} is not learned via OSPF"
+ quiet: true
+ loop: "{{ _ospf_unnumbered_v_routes.stdout | default([]) }}"
+ loop_control:
+ label: "{{ item | regex_search('Routing entry for \\S+') | default('route', true) }}"
+ when: ospf_unnumbered_group | length > 0
+
+- name: With two routers, one ECMP next hop per link
+ ansible.builtin.assert:
+ that: _ospf_unnumbered_v_routes.stdout[0] | regex_findall('via \S+ onlink') | length == ospf_unnumbered_interfaces | length
+ fail_msg: "expected {{ ospf_unnumbered_interfaces | length }} ECMP next hops to the peer"
+ quiet: true
+ when:
+ - ospf_unnumbered_group | length > 0
+ - groups[ospf_unnumbered_group] | length == 2
diff --git a/roles/ospf_unnumbered/templates/l3_interfaces.yml.j2 b/roles/ospf_unnumbered/templates/l3_interfaces.yml.j2
new file mode 100644
index 0000000..ccd1962
--- /dev/null
+++ b/roles/ospf_unnumbered/templates/l3_interfaces.yml.j2
@@ -0,0 +1,8 @@
+- name: lo
+ ipv4:
+ - address: {{ _ospf_unnumbered_prefix | to_json }}
+{% for i in ospf_unnumbered_interfaces %}
+- name: {{ i }}
+ ipv4:
+ - address: {{ _ospf_unnumbered_prefix | to_json }}
+{% endfor %}
diff --git a/roles/ospf_unnumbered/templates/ospf_interfaces.yml.j2 b/roles/ospf_unnumbered/templates/ospf_interfaces.yml.j2
new file mode 100644
index 0000000..c119640
--- /dev/null
+++ b/roles/ospf_unnumbered/templates/ospf_interfaces.yml.j2
@@ -0,0 +1,12 @@
+{% for i in ospf_unnumbered_interfaces %}
+- name: {{ i }}
+ address_family:
+ - afi: ipv4
+ network: point-to-point
+{% if ospf_unnumbered_md5_key %}
+ authentication:
+ md5_key:
+ key_id: {{ ospf_unnumbered_md5_key_id | int }}
+ key: {{ ospf_unnumbered_md5_key | to_json }}
+{% endif %}
+{% endfor %}
diff --git a/roles/ospf_unnumbered/templates/ospfv2.yml.j2 b/roles/ospf_unnumbered/templates/ospfv2.yml.j2
new file mode 100644
index 0000000..0b24f2b
--- /dev/null
+++ b/roles/ospf_unnumbered/templates/ospfv2.yml.j2
@@ -0,0 +1,15 @@
+parameters:
+ router_id: {{ ospf_unnumbered_router_id | to_json }}
+areas:
+ - area_id: {{ ospf_unnumbered_area | string | to_json }}
+{% if ospf_unnumbered_md5_key %}
+ authentication: md5
+{% endif %}
+ network:
+ - address: {{ _ospf_unnumbered_prefix | to_json }}
+{% if ospf_unnumbered_redistribute %}
+redistribute:
+{% for r in ospf_unnumbered_redistribute %}
+ - route_type: {{ r }}
+{% endfor %}
+{% endif %}
diff --git a/roles/ospf_unnumbered/vars/main.yml b/roles/ospf_unnumbered/vars/main.yml
new file mode 100644
index 0000000..3bf0497
--- /dev/null
+++ b/roles/ospf_unnumbered/vars/main.yml
@@ -0,0 +1,3 @@
+---
+_ospf_unnumbered_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_ospf_unnumbered_prefix: "{{ ospf_unnumbered_router_id }}/32"
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 }}"
diff --git a/roles/zone_firewall/defaults/main.yml b/roles/zone_firewall/defaults/main.yml
new file mode 100644
index 0000000..6bb4a1a
--- /dev/null
+++ b/roles/zone_firewall/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+zone_firewall_policies: []
+zone_firewall_ipv6: true
+zone_firewall_all_pairs: true
+zone_firewall_base_rules: true
+zone_firewall_log_rules: true
diff --git a/roles/zone_firewall/meta/argument_specs.yml b/roles/zone_firewall/meta/argument_specs.yml
new file mode 100644
index 0000000..fa47473
--- /dev/null
+++ b/roles/zone_firewall/meta/argument_specs.yml
@@ -0,0 +1,151 @@
+---
+argument_specs:
+ main:
+ short_description: Zone-based firewall (docs blueprint "Zone-Policy example")
+ description:
+ - Defines zones, one ruleset per zone-pair-direction named
+ C(<from>-<to>) for IPv4 and C(<from>-<to>-6) for IPv6, and binds the
+ rulesets to the zones, following the practices on the docs page.
+ - Every ruleset gets C(default-action drop), C(default-log) and the two
+ base rules from the page (1 accept established/related, 2 drop and log
+ invalid). Rules 1 and 2 are therefore reserved.
+ - With C(zone_firewall_all_pairs), rulesets are created for every zone
+ pair, including pairs that never communicate, so attempts are logged.
+ - Uses vyos.vyos.vyos_firewall_rules and vyos.vyos.vyos_firewall_global
+ (zone support requires a vyos.vyos release that includes it).
+ - B(Lockout risk) as the page warns - put the interface you manage the
+ router through into a zone with rules allowing SSH to the local zone
+ before running this role.
+ options:
+ zone_firewall_zones:
+ type: list
+ elements: dict
+ required: true
+ description: Zones. Exactly one may be the local zone (the router itself).
+ options:
+ name:
+ type: str
+ required: true
+ description: Zone name, e.g. C(lan).
+ interfaces:
+ type: list
+ elements: str
+ default: []
+ description: Member interfaces, e.g. C([eth0.20]). Not used for the local zone.
+ local:
+ type: bool
+ default: false
+ description: This zone is the router itself.
+ default_action:
+ type: str
+ choices: [drop, reject]
+ default: drop
+ description: Zone default action (zones cannot accept).
+ default_log:
+ type: bool
+ default: true
+ description: Log packets hitting the zone default action.
+ description:
+ type: str
+ description: Zone description.
+ zone_firewall_policies:
+ type: list
+ elements: dict
+ default: []
+ description: Rules per zone-pair-direction (traffic from C(from) to C(to)).
+ options:
+ from:
+ type: str
+ required: true
+ description: Source zone.
+ to:
+ type: str
+ required: true
+ description: Destination zone.
+ default_action:
+ type: str
+ choices: [drop, reject, accept]
+ default: drop
+ description: Ruleset default action, e.g. C(accept) for a management zone.
+ rules:
+ type: list
+ elements: dict
+ default: []
+ description: Rules added after the base rules.
+ options:
+ number:
+ type: int
+ required: true
+ description: Rule number (not 1 or 2 while base rules are on).
+ action:
+ type: str
+ choices: [accept, drop, reject]
+ default: accept
+ description: Rule action.
+ description:
+ type: str
+ description: Rule description.
+ protocol:
+ type: str
+ description:
+ - Protocol, e.g. C(tcp), C(udp), C(tcp_udp), C(icmp).
+ - C(icmp) becomes C(ipv6-icmp) in the IPv6 ruleset.
+ source:
+ type: dict
+ description: Source match.
+ options:
+ address:
+ type: str
+ description: Address or network.
+ port:
+ type: str
+ description: Port(s), e.g. C(22) or C(20,21).
+ destination:
+ type: dict
+ description: Destination match.
+ options:
+ address:
+ type: str
+ description: Address or network.
+ port:
+ type: str
+ description: Port(s), e.g. C(80,443).
+ family:
+ type: str
+ choices: [ipv4, ipv6, both]
+ description:
+ - Rulesets the rule goes into. Inferred when omitted - an
+ IPv6 address means C(ipv6), an IPv4 address C(ipv4), no
+ address C(both).
+ log:
+ type: bool
+ description: Log matches. Defaults to C(zone_firewall_log_rules).
+ zone_firewall_ipv6:
+ type: bool
+ default: true
+ description: Also create IPv6 rulesets and bind them.
+ zone_firewall_all_pairs:
+ type: bool
+ default: true
+ description: Create a (base rules only) ruleset for every zone pair not listed.
+ zone_firewall_base_rules:
+ type: bool
+ default: true
+ description: Add the page's rules 1 (established/related) and 2 (invalid) to every ruleset.
+ zone_firewall_log_rules:
+ type: bool
+ default: true
+ description: Default for C(log) on user rules, as the page recommends.
+ 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 zone_firewall role
+ description: Run with C(tasks_from=verify). Checks every zone and its member interfaces are active.
+ options:
+ zone_firewall_zones:
+ type: list
+ elements: dict
+ required: true
+ description: Same value as for C(main).
diff --git a/roles/zone_firewall/meta/main.yml b/roles/zone_firewall/meta/main.yml
new file mode 100644
index 0000000..c83c9f4
--- /dev/null
+++ b/roles/zone_firewall/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Zone-based firewall with per zone-pair rulesets for IPv4 and IPv6
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, firewall, zone]
+dependencies: []
diff --git a/roles/zone_firewall/tasks/main.yml b/roles/zone_firewall/tasks/main.yml
new file mode 100644
index 0000000..469dc24
--- /dev/null
+++ b/roles/zone_firewall/tasks/main.yml
@@ -0,0 +1,34 @@
+---
+- name: Check zones and policies
+ ansible.builtin.assert:
+ that:
+ - zone_firewall_zones | selectattr('local', 'defined') | selectattr('local') | list | length <= 1
+ - (zone_firewall_policies | map(attribute='from') | list + zone_firewall_policies | map(attribute='to') | list)
+ | difference(_zone_firewall_names) | length == 0
+ - not (zone_firewall_base_rules | bool)
+ or (zone_firewall_policies | map(attribute='rules', default=[]) | flatten
+ | map(attribute='number') | select('in', [1, 2]) | list | length == 0)
+ fail_msg: >-
+ At most one local zone; every policy must reference defined zones;
+ rule numbers 1 and 2 are reserved for the base rules.
+ quiet: true
+
+- name: Create one ruleset per zone-pair-direction
+ vyos.vyos.vyos_firewall_rules:
+ config: "{{ lookup('ansible.builtin.template', 'firewall_rules.yml.j2') | from_yaml }}"
+ state: "{{ _zone_firewall_state }}"
+ register: _zone_firewall_r_rules
+
+- name: Define zones and bind the rulesets
+ vyos.vyos.vyos_firewall_global:
+ config: "{{ lookup('ansible.builtin.template', 'zones.yml.j2') | from_yaml }}"
+ state: "{{ _zone_firewall_state }}"
+ register: _zone_firewall_r_zones
+
+- 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([])
+ + ([_zone_firewall_r_rules, _zone_firewall_r_zones]
+ | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/zone_firewall/tasks/verify.yml b/roles/zone_firewall/tasks/verify.yml
new file mode 100644
index 0000000..110eeac
--- /dev/null
+++ b/roles/zone_firewall/tasks/verify.yml
@@ -0,0 +1,24 @@
+---
+- name: Read zone policy
+ vyos.vyos.vyos_command:
+ commands:
+ - show firewall zone-policy
+ register: _zone_firewall_v
+
+- name: Every zone is active
+ ansible.builtin.assert:
+ that: _zone_firewall_v.stdout[0] is search('(?m)^\s*' ~ (item.name | regex_escape) ~ '\s')
+ fail_msg: "zone {{ item.name }} is missing from 'show firewall zone-policy'"
+ quiet: true
+ loop: "{{ zone_firewall_zones }}"
+ loop_control:
+ label: "{{ item.name }}"
+
+- name: Member interfaces are bound
+ ansible.builtin.assert:
+ that: _zone_firewall_v.stdout[0] is search('\b' ~ (item.1 | regex_escape) ~ '\b')
+ fail_msg: "interface {{ item.1 }} of zone {{ item.0.name }} is not bound"
+ quiet: true
+ loop: "{{ zone_firewall_zones | subelements('interfaces', skip_missing=true) }}"
+ loop_control:
+ label: "{{ item.0.name }} {{ item.1 }}"
diff --git a/roles/zone_firewall/templates/firewall_rules.yml.j2 b/roles/zone_firewall/templates/firewall_rules.yml.j2
new file mode 100644
index 0000000..0e083f3
--- /dev/null
+++ b/roles/zone_firewall/templates/firewall_rules.yml.j2
@@ -0,0 +1,47 @@
+{%- macro rule_family(r) -%}
+{%- set addrs = [r.source.address | default('') if r.source is defined and r.source else '',
+ r.destination.address | default('') if r.destination is defined and r.destination else ''] | select | list -%}
+{%- if r.family is defined and r.family -%}{{ r.family }}
+{%- elif addrs | select('search', ':') | list -%}ipv6
+{%- elif addrs -%}ipv4
+{%- else -%}both
+{%- endif -%}
+{%- endmacro -%}
+{% for afi in _zone_firewall_families %}
+- afi: {{ afi }}
+ rule_sets:
+{% for p in _zone_firewall_policies %}
+ - name: {{ (p['from'] ~ '-' ~ p['to'] ~ ('-6' if afi == 'ipv6' else '')) | to_json }}
+ default_action: {{ p.default_action | default('drop') }}
+ enable_default_log: true
+ rules:
+{% if zone_firewall_base_rules | bool %}
+ - number: 1
+ action: accept
+ state: {established: true, related: true}
+ - number: 2
+ action: drop
+ log: enable
+ state: {invalid: true}
+{% endif %}
+{% for r in p.rules | default([]) if rule_family(r) in [afi, 'both'] %}
+ - 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: {{ ('ipv6-icmp' if (afi == 'ipv6' and r.protocol == 'icmp') else r.protocol) | to_json }}
+{% endif %}
+{% if r.log | default(zone_firewall_log_rules) | bool %}
+ log: enable
+{% endif %}
+{% 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 %}
+{% endfor %}
+{% endfor %}
diff --git a/roles/zone_firewall/templates/zones.yml.j2 b/roles/zone_firewall/templates/zones.yml.j2
new file mode 100644
index 0000000..3af0e99
--- /dev/null
+++ b/roles/zone_firewall/templates/zones.yml.j2
@@ -0,0 +1,28 @@
+zone:
+{% for z in zone_firewall_zones %}
+ - name: {{ z.name | to_json }}
+ default_action: {{ z.default_action | default('drop') }}
+{% if z.default_log | default(true) | bool %}
+ default_log: true
+{% endif %}
+{% if z.description is defined %}
+ description: {{ z.description | to_json }}
+{% endif %}
+{% if z.local | default(false) | bool %}
+ local_zone: true
+{% else %}
+ interfaces: {{ z.interfaces | default([]) | to_json }}
+{% endif %}
+{% set srcs = _zone_firewall_policies | selectattr('to', 'equalto', z.name) | list %}
+{% if srcs %}
+ sources:
+{% for p in srcs %}
+ - zone: {{ p['from'] | to_json }}
+ firewall:
+ name: {{ (p['from'] ~ '-' ~ z.name) | to_json }}
+{% if zone_firewall_ipv6 | bool %}
+ ipv6_name: {{ (p['from'] ~ '-' ~ z.name ~ '-6') | to_json }}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
diff --git a/roles/zone_firewall/vars/main.yml b/roles/zone_firewall/vars/main.yml
new file mode 100644
index 0000000..14ffa31
--- /dev/null
+++ b/roles/zone_firewall/vars/main.yml
@@ -0,0 +1,17 @@
+---
+_zone_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_zone_firewall_families: "{{ ['ipv4', 'ipv6'] if zone_firewall_ipv6 | bool else ['ipv4'] }}"
+_zone_firewall_names: "{{ zone_firewall_zones | map(attribute='name') | list }}"
+# Listed policies plus, with zone_firewall_all_pairs, an empty policy for every
+# other ordered zone pair, so every zone-pair-direction has a logged ruleset.
+_zone_firewall_policies: >-
+ {%- set out = zone_firewall_policies | list -%}
+ {%- if zone_firewall_all_pairs | bool -%}
+ {%- set listed = zone_firewall_policies | map(attribute='from') | zip(zone_firewall_policies | map(attribute='to')) | map('join', '>') | list -%}
+ {%- for a in _zone_firewall_names -%}
+ {%- for b in _zone_firewall_names if b != a and (a ~ '>' ~ b) not in listed -%}
+ {%- set _ = out.append({'from': a, 'to': b, 'rules': []}) -%}
+ {%- endfor -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {{ out }}