summaryrefslogtreecommitdiff
path: root/roles/ipsec_route_based
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/ipsec_route_based
parent748df2bc1d35fa285dd3fe46916e1230408778c7 (diff)
downloadvyos.blueprints-main.tar.gz
vyos.blueprints-main.zip
T9393: new rolesmain
Diffstat (limited to 'roles/ipsec_route_based')
-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
16 files changed, 601 insertions, 0 deletions
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 }}