summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/package-smoketest.yml2
-rw-r--r--.github/workflows/trigger-pr.yml6
-rw-r--r--data/templates/firewall/nftables-bridge.j285
-rw-r--r--data/templates/firewall/nftables-defines.j210
-rw-r--r--data/templates/firewall/nftables.j2101
-rw-r--r--data/templates/firewall/sysctl-firewall.conf.j28
-rw-r--r--data/templates/frr/static_mcast.frr.j29
-rw-r--r--data/templates/ipsec/ios_profile.j29
-rw-r--r--data/templates/ipsec/swanctl.conf.j22
-rw-r--r--data/templates/ipsec/swanctl/remote_access.j29
-rw-r--r--data/templates/ipsec/windows_profile.j22
-rw-r--r--data/templates/zabbix-agent/10-override.conf.j25
-rw-r--r--interface-definitions/firewall.xml.in3
-rw-r--r--interface-definitions/include/firewall/address-inet.xml.i63
-rw-r--r--interface-definitions/include/firewall/address-mask-inet.xml.i19
-rw-r--r--interface-definitions/include/firewall/bridge-custom-name.xml.i6
-rw-r--r--interface-definitions/include/firewall/bridge-hook-forward.xml.i6
-rw-r--r--interface-definitions/include/firewall/bridge-hook-input.xml.i40
-rw-r--r--interface-definitions/include/firewall/bridge-hook-output.xml.i40
-rw-r--r--interface-definitions/include/firewall/bridge-hook-prerouting.xml.i37
-rw-r--r--interface-definitions/include/firewall/common-rule-bridge.xml.i34
-rw-r--r--interface-definitions/include/firewall/global-options.xml.i19
-rw-r--r--interface-definitions/include/firewall/set-packet-modifications.xml.i96
-rw-r--r--interface-definitions/include/firewall/source-destination-group-inet.xml.i50
-rw-r--r--interface-definitions/include/ipsec/bind.xml.i10
-rw-r--r--interface-definitions/include/policy/route-common.xml.i77
-rw-r--r--interface-definitions/service_monitoring_zabbix-agent.xml.in1
-rw-r--r--interface-definitions/vpn_ipsec.xml.in49
-rw-r--r--op-mode-definitions/restart-serial.xml.in31
-rw-r--r--python/vyos/configtree.py9
-rw-r--r--python/vyos/defaults.py10
-rw-r--r--python/vyos/firewall.py70
-rw-r--r--python/vyos/ifconfig/vti.py19
-rw-r--r--python/vyos/utils/serial.py118
-rw-r--r--python/vyos/utils/vti_updown_db.py194
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py33
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vti.py3
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py49
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static_multicast.py49
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py275
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py22
-rw-r--r--src/completion/list_login_ttys.py25
-rwxr-xr-xsrc/conf_mode/firewall.py163
-rwxr-xr-xsrc/conf_mode/interfaces_l2tpv3.py3
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py6
-rwxr-xr-xsrc/conf_mode/interfaces_wireless.py3
-rwxr-xr-xsrc/conf_mode/nat.py3
-rwxr-xr-xsrc/conf_mode/nat64.py18
-rwxr-xr-xsrc/conf_mode/nat66.py3
-rwxr-xr-xsrc/conf_mode/policy_route.py29
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py31
-rwxr-xr-xsrc/conf_mode/system_acceleration.py3
-rwxr-xr-xsrc/conf_mode/system_console.py14
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py129
-rwxr-xr-xsrc/conf_mode/vrf.py17
-rwxr-xr-xsrc/etc/ipsec.d/vti-up-down53
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf5
-rw-r--r--src/migration-scripts/openvpn/1-to-28
-rw-r--r--src/migration-scripts/openvpn/2-to-38
-rw-r--r--src/migration-scripts/openvpn/3-to-46
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py113
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py85
-rwxr-xr-xsrc/op_mode/ipsec.py1
-rw-r--r--src/op_mode/serial.py38
-rw-r--r--src/tests/test_configd_inspect.py3
65 files changed, 2075 insertions, 372 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml
index 824cd64b1..0a8208b87 100644
--- a/.github/workflows/package-smoketest.yml
+++ b/.github/workflows/package-smoketest.yml
@@ -1,7 +1,7 @@
name: VyOS ISO integration Test
on:
- pull_request_target:
+ pull_request:
branches:
- current
paths:
diff --git a/.github/workflows/trigger-pr.yml b/.github/workflows/trigger-pr.yml
index 0e28b460f..f88458a81 100644
--- a/.github/workflows/trigger-pr.yml
+++ b/.github/workflows/trigger-pr.yml
@@ -5,13 +5,13 @@ on:
types:
- closed
branches:
- - current
-
+ - circinus
+
jobs:
trigger-PR:
uses: vyos/.github/.github/workflows/trigger-pr.yml@current
with:
- source_branch: 'current'
+ source_branch: 'circinus'
target_branch: 'circinus'
secrets:
REMOTE_REPO: ${{ secrets.REMOTE_REPO }}
diff --git a/data/templates/firewall/nftables-bridge.j2 b/data/templates/firewall/nftables-bridge.j2
index dec027bf9..1975fb9b0 100644
--- a/data/templates/firewall/nftables-bridge.j2
+++ b/data/templates/firewall/nftables-bridge.j2
@@ -1,9 +1,13 @@
+{% import 'firewall/nftables-defines.j2' as group_tmpl %}
{% macro bridge(bridge) %}
{% set ns = namespace(sets=[]) %}
{% if bridge.forward is vyos_defined %}
{% for prior, conf in bridge.forward.items() %}
chain VYOS_FORWARD_{{ prior }} {
type filter hook forward priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('FWD', prior, rule_id, 'bri') }}
@@ -17,6 +21,46 @@
{% endfor %}
{% endif %}
+{% if bridge.input is vyos_defined %}
+{% for prior, conf in bridge.input.items() %}
+ chain VYOS_INPUT_{{ prior }} {
+ type filter hook input priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('INP', prior, rule_id, 'bri') }}
+{% if rule_conf.recent is vyos_defined %}
+{% set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %}
+{% endif %}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('INP-filter', 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% if bridge.output is vyos_defined %}
+{% for prior, conf in bridge.output.items() %}
+ chain VYOS_OUTUT_{{ prior }} {
+ type filter hook output priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('OUT', prior, rule_id, 'bri') }}
+{% if rule_conf.recent is vyos_defined %}
+{% set ns.sets = ns.sets + ['OUT_' + prior + '_' + rule_id] %}
+{% endif %}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('OUT-filter', 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
{% if bridge.name is vyos_defined %}
{% for name_text, conf in bridge.name.items() %}
chain NAME_{{ name_text }} {
@@ -32,4 +76,45 @@
}
{% endfor %}
{% endif %}
+
+{% for set_name in ns.sets %}
+ set RECENT_{{ set_name }} {
+ type ipv4_addr
+ size 65535
+ flags dynamic
+ }
+{% endfor %}
+{% for set_name in ip_fqdn %}
+ set FQDN_{{ set_name }} {
+ type ipv4_addr
+ flags interval
+ }
+{% endfor %}
+{% if geoip_updated.name is vyos_defined %}
+{% for setname in geoip_updated.name %}
+ set {{ setname }} {
+ type ipv4_addr
+ flags interval
+ }
+{% endfor %}
+{% endif %}
+
+{{ group_tmpl.groups(group, False, True) }}
+{{ group_tmpl.groups(group, True, True) }}
+
+{% if global_options.state_policy is vyos_defined %}
+ chain VYOS_STATE_POLICY {
+{% if global_options.state_policy.established is vyos_defined %}
+ {{ global_options.state_policy.established | nft_state_policy('established') }}
+{% endif %}
+{% if global_options.state_policy.invalid is vyos_defined %}
+ {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+{% endif %}
+{% if global_options.state_policy.related is vyos_defined %}
+ {{ global_options.state_policy.related | nft_state_policy('related') }}
+{% endif %}
+ return
+ }
+{% endif %}
+
{% endmacro %}
diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2
index 8a75ab2d6..fa6cd74c0 100644
--- a/data/templates/firewall/nftables-defines.j2
+++ b/data/templates/firewall/nftables-defines.j2
@@ -1,7 +1,7 @@
{% macro groups(group, is_ipv6, is_l3) %}
{% if group is vyos_defined %}
{% set ip_type = 'ipv6_addr' if is_ipv6 else 'ipv4_addr' %}
-{% if group.address_group is vyos_defined and not is_ipv6 and is_l3 %}
+{% if group.address_group is vyos_defined and not is_ipv6 %}
{% for group_name, group_conf in group.address_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
set A_{{ group_name }} {
@@ -14,7 +14,7 @@
}
{% endfor %}
{% endif %}
-{% if group.ipv6_address_group is vyos_defined and is_ipv6 and is_l3 %}
+{% if group.ipv6_address_group is vyos_defined and is_ipv6 %}
{% for group_name, group_conf in group.ipv6_address_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
set A6_{{ group_name }} {
@@ -46,7 +46,7 @@
}
{% endfor %}
{% endif %}
-{% if group.network_group is vyos_defined and not is_ipv6 and is_l3 %}
+{% if group.network_group is vyos_defined and not is_ipv6 %}
{% for group_name, group_conf in group.network_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
set N_{{ group_name }} {
@@ -59,7 +59,7 @@
}
{% endfor %}
{% endif %}
-{% if group.ipv6_network_group is vyos_defined and is_ipv6 and is_l3 %}
+{% if group.ipv6_network_group is vyos_defined and is_ipv6 %}
{% for group_name, group_conf in group.ipv6_network_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
set N6_{{ group_name }} {
@@ -72,7 +72,7 @@
}
{% endfor %}
{% endif %}
-{% if group.port_group is vyos_defined and is_l3 %}
+{% if group.port_group is vyos_defined %}
{% for group_name, group_conf in group.port_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
set P_{{ group_name }} {
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index 68a3bfd87..82dcefac0 100644
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -339,7 +339,104 @@ table ip6 vyos_filter {
delete table bridge vyos_filter
{% endif %}
table bridge vyos_filter {
-{{ bridge_tmpl.bridge(bridge) }}
+{% if bridge is vyos_defined %}
+{% if bridge.forward is vyos_defined %}
+{% for prior, conf in bridge.forward.items() %}
+ chain VYOS_FORWARD_{{ prior }} {
+ type filter hook forward priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('FWD', prior, rule_id, 'bri') }}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('FWD-' + prior, 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% if bridge.input is vyos_defined %}
+{% for prior, conf in bridge.input.items() %}
+ chain VYOS_INPUT_{{ prior }} {
+ type filter hook input priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('INP', prior, rule_id, 'bri') }}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('INP-' + prior, 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% if bridge.output is vyos_defined %}
+{% for prior, conf in bridge.output.items() %}
+ chain VYOS_OUTUT_{{ prior }} {
+ type filter hook output priority {{ prior }}; policy accept;
+{% if global_options.state_policy is vyos_defined %}
+ jump VYOS_STATE_POLICY
+{% endif %}
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('OUT', prior, rule_id, 'bri') }}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('OUT-' + prior, 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% if bridge.prerouting is vyos_defined %}
+{% for prior, conf in bridge.prerouting.items() %}
+ chain VYOS_PREROUTING_{{ prior }} {
+ type filter hook prerouting priority {{ prior }}; policy accept;
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('PRE', prior, rule_id, 'bri') }}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule('PRE-' + prior, 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% if bridge.name is vyos_defined %}
+{% for name_text, conf in bridge.name.items() %}
+ chain NAME_{{ name_text }} {
+{% if conf.rule is vyos_defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
+ {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'bri') }}
+{% if rule_conf.recent is vyos_defined %}
+{% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %}
+{% endif %}
+{% endfor %}
+{% endif %}
+ {{ conf | nft_default_rule(name_text, 'bri') }}
+ }
+{% endfor %}
+{% endif %}
+
+{% endif %}
{{ group_tmpl.groups(group, False, False) }}
+{{ group_tmpl.groups(group, True, False) }}
-}
+{% if global_options.state_policy is vyos_defined %}
+ chain VYOS_STATE_POLICY {
+{% if global_options.state_policy.established is vyos_defined %}
+ {{ global_options.state_policy.established | nft_state_policy('established') }}
+{% endif %}
+{% if global_options.state_policy.invalid is vyos_defined %}
+ {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+{% endif %}
+{% if global_options.state_policy.related is vyos_defined %}
+ {{ global_options.state_policy.related | nft_state_policy('related') }}
+{% endif %}
+ return
+ }
+{% endif %}
+} \ No newline at end of file
diff --git a/data/templates/firewall/sysctl-firewall.conf.j2 b/data/templates/firewall/sysctl-firewall.conf.j2
index b9c3311e2..6c33ffdc8 100644
--- a/data/templates/firewall/sysctl-firewall.conf.j2
+++ b/data/templates/firewall/sysctl-firewall.conf.j2
@@ -13,6 +13,14 @@ net.ipv4.conf.*.send_redirects = {{ 1 if global_options.send_redirects == 'enabl
net.ipv4.tcp_syncookies = {{ 1 if global_options.syn_cookies == 'enable' else 0 }}
net.ipv4.tcp_rfc1337 = {{ 1 if global_options.twa_hazards_protection == 'enable' else 0 }}
+{% if global_options.apply_to_bridged_traffic is vyos_defined %}
+net.bridge.bridge-nf-call-iptables = {{ 1 if global_options.apply_to_bridged_traffic.ipv4 is vyos_defined else 0 }}
+net.bridge.bridge-nf-call-ip6tables = {{ 1 if global_options.apply_to_bridged_traffic.ipv6 is vyos_defined else 0 }}
+{% else %}
+net.bridge.bridge-nf-call-iptables = 0
+net.bridge.bridge-nf-call-ip6tables = 0
+{% endif %}
+
## Timeout values:
net.netfilter.nf_conntrack_icmp_timeout = {{ global_options.timeout.icmp }}
net.netfilter.nf_conntrack_generic_timeout = {{ global_options.timeout.other }}
diff --git a/data/templates/frr/static_mcast.frr.j2 b/data/templates/frr/static_mcast.frr.j2
index 491d4b54a..54b2790b0 100644
--- a/data/templates/frr/static_mcast.frr.j2
+++ b/data/templates/frr/static_mcast.frr.j2
@@ -1,13 +1,4 @@
!
-{% for route_gr in old_mroute %}
-{% for nh in old_mroute[route_gr] %}
-{% if old_mroute[route_gr][nh] %}
-no ip mroute {{ route_gr }} {{ nh }} {{ old_mroute[route_gr][nh] }}
-{% else %}
-no ip mroute {{ route_gr }} {{ nh }}
-{% endif %}
-{% endfor %}
-{% endfor %}
{% for route_gr in mroute %}
{% for nh in mroute[route_gr] %}
{% if mroute[route_gr][nh] %}
diff --git a/data/templates/ipsec/ios_profile.j2 b/data/templates/ipsec/ios_profile.j2
index 935acbf8e..966fad433 100644
--- a/data/templates/ipsec/ios_profile.j2
+++ b/data/templates/ipsec/ios_profile.j2
@@ -55,9 +55,11 @@
<!-- The server is authenticated using a certificate -->
<key>AuthenticationMethod</key>
<string>Certificate</string>
+{% if authentication.client_mode.startswith("eap") %}
<!-- The client uses EAP to authenticate -->
<key>ExtendedAuthEnabled</key>
<integer>1</integer>
+{% endif %}
<!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES.
IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration -->
<key>IKESecurityAssociationParameters</key>
@@ -78,9 +80,14 @@
<string>{{ esp_encryption.encryption }}</string>
<key>IntegrityAlgorithm</key>
<string>{{ esp_encryption.hash }}</string>
+{% if esp_encryption.pfs is vyos_defined %}
<key>DiffieHellmanGroup</key>
- <integer>{{ ike_encryption.dh_group }}</integer>
+ <integer>{{ esp_encryption.pfs }}</integer>
+{% endif %}
</dict>
+ <!-- Controls whether the client offers Perfect Forward Secrecy (PFS). This should be set to match the server. -->
+ <key>EnablePFS</key>
+ <integer>{{ '1' if esp_encryption.pfs is vyos_defined else '0' }}</integer>
</dict>
</dict>
{% if ca_certificates is vyos_defined %}
diff --git a/data/templates/ipsec/swanctl.conf.j2 b/data/templates/ipsec/swanctl.conf.j2
index d44d0f5e4..698a9135e 100644
--- a/data/templates/ipsec/swanctl.conf.j2
+++ b/data/templates/ipsec/swanctl.conf.j2
@@ -31,6 +31,8 @@ pools {
{{ pool }} {
{% if pool_config.prefix is vyos_defined %}
addrs = {{ pool_config.prefix }}
+{% elif pool_config.range is vyos_defined %}
+ addrs = {{ pool_config.range.start }}-{{ pool_config.range.stop }}
{% endif %}
{% if pool_config.name_server is vyos_defined %}
dns = {{ pool_config.name_server | join(',') }}
diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2
index e384ae972..c79f292b4 100644
--- a/data/templates/ipsec/swanctl/remote_access.j2
+++ b/data/templates/ipsec/swanctl/remote_access.j2
@@ -46,7 +46,7 @@
{% endif %}
}
children {
- ikev2-vpn {
+ {{ name }}-client {
esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }}
{% if esp.life_bytes is vyos_defined %}
life_bytes = {{ esp.life_bytes }}
@@ -69,6 +69,13 @@
{% set local_port = rw_conf.local.port if rw_conf.local.port is vyos_defined else '' %}
{% set local_suffix = '[%any/{1}]'.format(local_port) if local_port else '' %}
local_ts = {{ local_prefix | join(local_suffix + ",") }}{{ local_suffix }}
+{% if rw_conf.bind is vyos_defined %}
+{# The key defaults to 0 and will match any policies which similarly do not have a lookup key configuration. #}
+{# Thus we simply shift the key by one to also support a vti0 interface #}
+{% set if_id = rw_conf.bind | replace('vti', '') | int + 1 %}
+ if_id_in = {{ if_id }}
+ if_id_out = {{ if_id }}
+{% endif %}
}
}
}
diff --git a/data/templates/ipsec/windows_profile.j2 b/data/templates/ipsec/windows_profile.j2
index 8c26944be..b5042f987 100644
--- a/data/templates/ipsec/windows_profile.j2
+++ b/data/templates/ipsec/windows_profile.j2
@@ -1,4 +1,4 @@
Remove-VpnConnection -Name "{{ vpn_name }}" -Force -PassThru
Add-VpnConnection -Name "{{ vpn_name }}" -ServerAddress "{{ remote }}" -TunnelType "Ikev2"
-Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup None -DHGroup "Group{{ ike_encryption.dh_group }}" -PassThru -Force
+Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup {{ esp_encryption.pfs }} -DHGroup {{ ike_encryption.dh_group }} -PassThru -Force
diff --git a/data/templates/zabbix-agent/10-override.conf.j2 b/data/templates/zabbix-agent/10-override.conf.j2
index 7c296e8fd..f6bd6500d 100644
--- a/data/templates/zabbix-agent/10-override.conf.j2
+++ b/data/templates/zabbix-agent/10-override.conf.j2
@@ -1,3 +1,4 @@
+{% set zabbix_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
[Unit]
After=
After=vyos-router.service
@@ -5,9 +6,11 @@ ConditionPathExists=
ConditionPathExists=/run/zabbix/zabbix-agent2.conf
[Service]
+User=
+User=root
EnvironmentFile=
ExecStart=
-ExecStart=/usr/sbin/zabbix_agent2 --config /run/zabbix/zabbix-agent2.conf --foreground
+ExecStart={{ zabbix_command }}/usr/sbin/zabbix_agent2 --config /run/zabbix/zabbix-agent2.conf --foreground
WorkingDirectory=
WorkingDirectory=/run/zabbix
Restart=always
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index dc4625af0..816dd1855 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -367,6 +367,9 @@
</properties>
<children>
#include <include/firewall/bridge-hook-forward.xml.i>
+ #include <include/firewall/bridge-hook-input.xml.i>
+ #include <include/firewall/bridge-hook-output.xml.i>
+ #include <include/firewall/bridge-hook-prerouting.xml.i>
#include <include/firewall/bridge-custom-name.xml.i>
</children>
</node>
diff --git a/interface-definitions/include/firewall/address-inet.xml.i b/interface-definitions/include/firewall/address-inet.xml.i
new file mode 100644
index 000000000..02ed8f6e4
--- /dev/null
+++ b/interface-definitions/include/firewall/address-inet.xml.i
@@ -0,0 +1,63 @@
+<!-- include start from firewall/address-inet.xml.i -->
+<leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4</format>
+ <description>Match everything except the specified address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4net</format>
+ <description>Match everything except the specified prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4range</format>
+ <description>Match everything except the specified range</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Subnet to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6range</format>
+ <description>IP range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv6</format>
+ <description>Match everything except the specified address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv6net</format>
+ <description>Match everything except the specified prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv6range</format>
+ <description>Match everything except the specified range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv4-range"/>
+ <validator name="ipv4-address-exclude"/>
+ <validator name="ipv4-prefix-exclude"/>
+ <validator name="ipv4-range-exclude"/>
+ <validator name="ipv6"/>
+ <validator name="ipv6-exclude"/>
+ <validator name="ipv6-range"/>
+ <validator name="ipv6-range-exclude"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/address-mask-inet.xml.i b/interface-definitions/include/firewall/address-mask-inet.xml.i
new file mode 100644
index 000000000..e2a5927ab
--- /dev/null
+++ b/interface-definitions/include/firewall/address-mask-inet.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from firewall/address-mask-inet.xml.i -->
+<leafNode name="address-mask">
+ <properties>
+ <help>IP mask</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 mask to apply</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IP mask to apply</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/bridge-custom-name.xml.i b/interface-definitions/include/firewall/bridge-custom-name.xml.i
index 654493c0e..9a2a829d0 100644
--- a/interface-definitions/include/firewall/bridge-custom-name.xml.i
+++ b/interface-definitions/include/firewall/bridge-custom-name.xml.i
@@ -32,6 +32,12 @@
</properties>
<children>
#include <include/firewall/common-rule-bridge.xml.i>
+ #include <include/firewall/action-l2.xml.i>
+ #include <include/firewall/connection-mark.xml.i>
+ #include <include/firewall/connection-status.xml.i>
+ #include <include/firewall/state.xml.i>
+ #include <include/firewall/inbound-interface.xml.i>
+ #include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/include/firewall/bridge-hook-forward.xml.i b/interface-definitions/include/firewall/bridge-hook-forward.xml.i
index 99f66ec77..fcc981925 100644
--- a/interface-definitions/include/firewall/bridge-hook-forward.xml.i
+++ b/interface-definitions/include/firewall/bridge-hook-forward.xml.i
@@ -26,6 +26,12 @@
</properties>
<children>
#include <include/firewall/common-rule-bridge.xml.i>
+ #include <include/firewall/action-l2.xml.i>
+ #include <include/firewall/connection-mark.xml.i>
+ #include <include/firewall/connection-status.xml.i>
+ #include <include/firewall/state.xml.i>
+ #include <include/firewall/inbound-interface.xml.i>
+ #include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/include/firewall/bridge-hook-input.xml.i b/interface-definitions/include/firewall/bridge-hook-input.xml.i
new file mode 100644
index 000000000..f6a11f8da
--- /dev/null
+++ b/interface-definitions/include/firewall/bridge-hook-input.xml.i
@@ -0,0 +1,40 @@
+<!-- include start from firewall/bridge-hook-input.xml.i -->
+<node name="input">
+ <properties>
+ <help>Bridge input firewall</help>
+ </properties>
+ <children>
+ <node name="filter">
+ <properties>
+ <help>Bridge firewall input filter</help>
+ </properties>
+ <children>
+ #include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/default-log.xml.i>
+ #include <include/generic-description.xml.i>
+ <tagNode name="rule">
+ <properties>
+ <help>Bridge Firewall input filter rule number</help>
+ <valueHelp>
+ <format>u32:1-999999</format>
+ <description>Number for this firewall rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/firewall/common-rule-bridge.xml.i>
+ #include <include/firewall/action-l2.xml.i>
+ #include <include/firewall/connection-mark.xml.i>
+ #include <include/firewall/connection-status.xml.i>
+ #include <include/firewall/state.xml.i>
+ #include <include/firewall/inbound-interface.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/bridge-hook-output.xml.i b/interface-definitions/include/firewall/bridge-hook-output.xml.i
new file mode 100644
index 000000000..38b8b08ca
--- /dev/null
+++ b/interface-definitions/include/firewall/bridge-hook-output.xml.i
@@ -0,0 +1,40 @@
+<!-- include start from firewall/bridge-hook-output.xml.i -->
+<node name="output">
+ <properties>
+ <help>Bridge output firewall</help>
+ </properties>
+ <children>
+ <node name="filter">
+ <properties>
+ <help>Bridge firewall output filter</help>
+ </properties>
+ <children>
+ #include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/default-log.xml.i>
+ #include <include/generic-description.xml.i>
+ <tagNode name="rule">
+ <properties>
+ <help>Bridge Firewall output filter rule number</help>
+ <valueHelp>
+ <format>u32:1-999999</format>
+ <description>Number for this firewall rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/firewall/common-rule-bridge.xml.i>
+ #include <include/firewall/action-l2.xml.i>
+ #include <include/firewall/connection-mark.xml.i>
+ #include <include/firewall/connection-status.xml.i>
+ #include <include/firewall/state.xml.i>
+ #include <include/firewall/outbound-interface.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/bridge-hook-prerouting.xml.i b/interface-definitions/include/firewall/bridge-hook-prerouting.xml.i
new file mode 100644
index 000000000..ea567644f
--- /dev/null
+++ b/interface-definitions/include/firewall/bridge-hook-prerouting.xml.i
@@ -0,0 +1,37 @@
+<!-- include start from firewall/bridge-hook-prerouting.xml.i -->
+<node name="prerouting">
+ <properties>
+ <help>Bridge prerouting firewall</help>
+ </properties>
+ <children>
+ <node name="filter">
+ <properties>
+ <help>Bridge firewall prerouting filter</help>
+ </properties>
+ <children>
+ #include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/default-log.xml.i>
+ #include <include/generic-description.xml.i>
+ <tagNode name="rule">
+ <properties>
+ <help>Bridge firewall prerouting filter rule number</help>
+ <valueHelp>
+ <format>u32:1-999999</format>
+ <description>Number for this firewall rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Firewall rule number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/firewall/common-rule-bridge.xml.i>
+ #include <include/firewall/action-and-notrack.xml.i>
+ #include <include/firewall/inbound-interface.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/common-rule-bridge.xml.i b/interface-definitions/include/firewall/common-rule-bridge.xml.i
index dcdd970ac..9ae28f7be 100644
--- a/interface-definitions/include/firewall/common-rule-bridge.xml.i
+++ b/interface-definitions/include/firewall/common-rule-bridge.xml.i
@@ -1,15 +1,36 @@
<!-- include start from firewall/common-rule-bridge.xml.i -->
-#include <include/firewall/action-l2.xml.i>
+#include <include/generic-description.xml.i>
+#include <include/generic-disable-node.xml.i>
+#include <include/firewall/dscp.xml.i>
+#include <include/firewall/firewall-mark.xml.i>
+#include <include/firewall/fragment.xml.i>
+#include <include/firewall/hop-limit.xml.i>
+#include <include/firewall/icmp.xml.i>
+#include <include/firewall/icmpv6.xml.i>
+#include <include/firewall/limit.xml.i>
+#include <include/firewall/log.xml.i>
+#include <include/firewall/log-options.xml.i>
+#include <include/firewall/match-ipsec.xml.i>
+#include <include/firewall/match-vlan.xml.i>
#include <include/firewall/nft-queue.xml.i>
+#include <include/firewall/packet-options.xml.i>
+#include <include/firewall/protocol.xml.i>
+#include <include/firewall/tcp-flags.xml.i>
+#include <include/firewall/tcp-mss.xml.i>
+#include <include/firewall/time.xml.i>
+#include <include/firewall/ttl.xml.i>
<node name="destination">
<properties>
<help>Destination parameters</help>
</properties>
<children>
#include <include/firewall/mac-address.xml.i>
+ #include <include/firewall/address-inet.xml.i>
+ #include <include/firewall/address-mask-inet.xml.i>
+ #include <include/firewall/port.xml.i>
+ #include <include/firewall/source-destination-group-inet.xml.i>
</children>
</node>
-#include <include/generic-disable-node.xml.i>
<leafNode name="jump-target">
<properties>
<help>Set jump target. Action jump must be defined to use this setting</help>
@@ -18,17 +39,16 @@
</completionHelp>
</properties>
</leafNode>
-#include <include/firewall/log.xml.i>
-#include <include/firewall/log-options.xml.i>
<node name="source">
<properties>
<help>Source parameters</help>
</properties>
<children>
#include <include/firewall/mac-address.xml.i>
+ #include <include/firewall/address-inet.xml.i>
+ #include <include/firewall/address-mask-inet.xml.i>
+ #include <include/firewall/port.xml.i>
+ #include <include/firewall/source-destination-group-inet.xml.i>
</children>
</node>
-#include <include/firewall/inbound-interface.xml.i>
-#include <include/firewall/outbound-interface.xml.i>
-#include <include/firewall/match-vlan.xml.i>
<!-- include end -->
diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i
index 9039b76fd..cee8f1854 100644
--- a/interface-definitions/include/firewall/global-options.xml.i
+++ b/interface-definitions/include/firewall/global-options.xml.i
@@ -44,6 +44,25 @@
</properties>
<defaultValue>disable</defaultValue>
</leafNode>
+ <node name="apply-to-bridged-traffic">
+ <properties>
+ <help>Apply configured firewall rules to traffic switched by bridges</help>
+ </properties>
+ <children>
+ <leafNode name="ipv4">
+ <properties>
+ <help>Apply configured IPv4 firewall rules</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6">
+ <properties>
+ <help>Apply configured IPv6 firewall rules</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
<leafNode name="directed-broadcast">
<properties>
<help>Policy for handling IPv4 directed broadcast forwarding on all interfaces</help>
diff --git a/interface-definitions/include/firewall/set-packet-modifications.xml.i b/interface-definitions/include/firewall/set-packet-modifications.xml.i
new file mode 100644
index 000000000..ee019b64e
--- /dev/null
+++ b/interface-definitions/include/firewall/set-packet-modifications.xml.i
@@ -0,0 +1,96 @@
+<!-- include start from firewall/set-packet-modifications.xml.i -->
+<node name="set">
+ <properties>
+ <help>Packet modifications</help>
+ </properties>
+ <children>
+ <leafNode name="connection-mark">
+ <properties>
+ <help>Set connection mark</help>
+ <valueHelp>
+ <format>u32:0-2147483647</format>
+ <description>Connection mark</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dscp">
+ <properties>
+ <help>Set DSCP (Packet Differentiated Services Codepoint) bits</help>
+ <valueHelp>
+ <format>u32:0-63</format>
+ <description>DSCP number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-63"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mark">
+ <properties>
+ <help>Set packet mark</help>
+ <valueHelp>
+ <format>u32:1-2147483647</format>
+ <description>Packet mark</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="table">
+ <properties>
+ <help>Set the routing table for matched packets</help>
+ <valueHelp>
+ <format>u32:1-200</format>
+ <description>Table number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>main</format>
+ <description>Main table</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-200"/>
+ <regex>(main)</regex>
+ </constraint>
+ <completionHelp>
+ <list>main</list>
+ <path>protocols static table</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="vrf">
+ <properties>
+ <help>VRF to forward packet with</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>default</format>
+ <description>Forward into default global VRF</description>
+ </valueHelp>
+ <completionHelp>
+ <list>default</list>
+ <path>vrf name</path>
+ </completionHelp>
+ #include <include/constraint/vrf.xml.i>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-mss">
+ <properties>
+ <help>Set TCP Maximum Segment Size</help>
+ <valueHelp>
+ <format>u32:500-1460</format>
+ <description>Explicitly set TCP MSS value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 500-1460"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/source-destination-group-inet.xml.i b/interface-definitions/include/firewall/source-destination-group-inet.xml.i
new file mode 100644
index 000000000..174051624
--- /dev/null
+++ b/interface-definitions/include/firewall/source-destination-group-inet.xml.i
@@ -0,0 +1,50 @@
+<!-- include start from firewall/source-destination-group-inet.xml.i -->
+<node name="group">
+ <properties>
+ <help>Group</help>
+ </properties>
+ <children>
+ <leafNode name="ipv4-address-group">
+ <properties>
+ <help>Group of IPv4 addresses</help>
+ <completionHelp>
+ <path>firewall group address-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-address-group">
+ <properties>
+ <help>Group of IPv6 addresses</help>
+ <completionHelp>
+ <path>firewall group ipv6-address-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/firewall/mac-group.xml.i>
+ <leafNode name="ipv4-network-group">
+ <properties>
+ <help>Group of IPv4 networks</help>
+ <completionHelp>
+ <path>firewall group network-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="ipv6-network-group">
+ <properties>
+ <help>Group of IPv6 networks</help>
+ <completionHelp>
+ <path>firewall group ipv6-network-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="port-group">
+ <properties>
+ <help>Group of ports</help>
+ <completionHelp>
+ <path>firewall group port-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/ipsec/bind.xml.i b/interface-definitions/include/ipsec/bind.xml.i
new file mode 100644
index 000000000..edc46d403
--- /dev/null
+++ b/interface-definitions/include/ipsec/bind.xml.i
@@ -0,0 +1,10 @@
+<!-- include start from ipsec/bind.xml.i -->
+<leafNode name="bind">
+ <properties>
+ <help>VTI tunnel interface associated with this configuration</help>
+ <completionHelp>
+ <path>interfaces vti</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i
index 97795601e..19ffc0506 100644
--- a/interface-definitions/include/policy/route-common.xml.i
+++ b/interface-definitions/include/policy/route-common.xml.i
@@ -66,82 +66,7 @@
</leafNode>
</children>
</node>
-<node name="set">
- <properties>
- <help>Packet modifications</help>
- </properties>
- <children>
- <leafNode name="connection-mark">
- <properties>
- <help>Connection marking</help>
- <valueHelp>
- <format>u32:0-2147483647</format>
- <description>Connection marking</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-2147483647"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="dscp">
- <properties>
- <help>Packet Differentiated Services Codepoint (DSCP)</help>
- <valueHelp>
- <format>u32:0-63</format>
- <description>DSCP number</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-63"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="mark">
- <properties>
- <help>Packet marking</help>
- <valueHelp>
- <format>u32:1-2147483647</format>
- <description>Packet marking</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-2147483647"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="table">
- <properties>
- <help>Routing table to forward packet with</help>
- <valueHelp>
- <format>u32:1-200</format>
- <description>Table number</description>
- </valueHelp>
- <valueHelp>
- <format>main</format>
- <description>Main table</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-200"/>
- <regex>(main)</regex>
- </constraint>
- <completionHelp>
- <list>main</list>
- <path>protocols static table</path>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="tcp-mss">
- <properties>
- <help>TCP Maximum Segment Size</help>
- <valueHelp>
- <format>u32:500-1460</format>
- <description>Explicitly set TCP MSS value</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 500-1460"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
-</node>
+#include <include/firewall/set-packet-modifications.xml.i>
#include <include/firewall/state.xml.i>
#include <include/firewall/tcp-flags.xml.i>
#include <include/firewall/tcp-mss.xml.i>
diff --git a/interface-definitions/service_monitoring_zabbix-agent.xml.in b/interface-definitions/service_monitoring_zabbix-agent.xml.in
index 3754e9145..e44b31312 100644
--- a/interface-definitions/service_monitoring_zabbix-agent.xml.in
+++ b/interface-definitions/service_monitoring_zabbix-agent.xml.in
@@ -185,6 +185,7 @@
</properties>
<defaultValue>3</defaultValue>
</leafNode>
+ #include <include/interface/vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 4a7fde75b..d9d6fd93b 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -854,6 +854,7 @@
#include <include/dhcp-interface.xml.i>
#include <include/ipsec/local-traffic-selector.xml.i>
#include <include/ipsec/replay-window.xml.i>
+ #include <include/ipsec/bind.xml.i>
<leafNode name="timeout">
<properties>
<help>Timeout to close connection if no data is transmitted</help>
@@ -978,6 +979,45 @@
</constraint>
</properties>
</leafNode>
+ <node name="range">
+ <properties>
+ <help>Local IPv4 or IPv6 pool range</help>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address for local pool range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 start address of pool</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 start address of pool</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address for local pool range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 end address of pool</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 end address of pool</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
#include <include/name-server-ipv4-ipv6.xml.i>
</children>
</tagNode>
@@ -1201,14 +1241,7 @@
<help>Virtual tunnel interface</help>
</properties>
<children>
- <leafNode name="bind">
- <properties>
- <help>VTI tunnel interface associated with this configuration</help>
- <completionHelp>
- <path>interfaces vti</path>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/ipsec/bind.xml.i>
#include <include/ipsec/esp-group.xml.i>
</children>
</node>
diff --git a/op-mode-definitions/restart-serial.xml.in b/op-mode-definitions/restart-serial.xml.in
new file mode 100644
index 000000000..4d8a03633
--- /dev/null
+++ b/op-mode-definitions/restart-serial.xml.in
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="serial">
+ <properties>
+ <help>Restart services on serial ports</help>
+ </properties>
+ <children>
+ <node name="console">
+ <properties>
+ <help>Restart serial console service for login TTYs</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/serial.py restart_console</command>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>Restart specific TTY device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_login_ttys.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/serial.py restart_console --device-name "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 5775070e2..bd77ab899 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
@@ -290,7 +290,7 @@ class ConfigTree(object):
else:
return True
- def list_nodes(self, path):
+ def list_nodes(self, path, path_must_exist=True):
check_path(path)
path_str = " ".join(map(str, path)).encode()
@@ -298,7 +298,10 @@ class ConfigTree(object):
res = json.loads(res_json)
if res is None:
- raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ if path_must_exist:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return []
else:
return res
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9ccd925ce..25ee45391 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -50,3 +50,13 @@ commit_lock = os.path.join(directories['vyos_configdir'], '.lock')
component_version_json = os.path.join(directories['data'], 'component-versions.json')
config_default = os.path.join(directories['data'], 'config.boot.default')
+
+rt_symbolic_names = {
+ # Standard routing tables for Linux & reserved IDs for VyOS
+ 'default': 253, # Confusingly, a final fallthru, not the default.
+ 'main': 254, # The actual global table used by iproute2 unless told otherwise.
+ 'local': 255, # Special kernel loopback table.
+}
+
+rt_global_vrf = rt_symbolic_names['main']
+rt_global_table = rt_symbolic_names['main']
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 40399f481..cac6d2953 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -30,6 +30,9 @@ from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
# Conntrack
def conntrack_required(conf):
@@ -164,7 +167,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if address_mask:
operator = '!=' if exclude else '=='
operator = f'& {address_mask} {operator} '
- output.append(f'{ip_name} {prefix}addr {operator}{suffix}')
+ if is_ipv4(suffix):
+ output.append(f'ip {prefix}addr {operator}{suffix}')
+ else:
+ output.append(f'ip6 {prefix}addr {operator}{suffix}')
if 'fqdn' in side_conf:
fqdn = side_conf['fqdn']
@@ -233,22 +239,38 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'group' in side_conf:
group = side_conf['group']
- if 'address_group' in group:
- group_name = group['address_group']
- operator = ''
- exclude = group_name[0] == "!"
- if exclude:
- operator = '!='
- group_name = group_name[1:]
- if address_mask:
- operator = '!=' if exclude else '=='
- operator = f'& {address_mask} {operator}'
- output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
- elif 'dynamic_address_group' in group:
+ for ipvx_address_group in ['address_group', 'ipv4_address_group', 'ipv6_address_group']:
+ if ipvx_address_group in group:
+ group_name = group[ipvx_address_group]
+ operator = ''
+ exclude = group_name[0] == "!"
+ if exclude:
+ operator = '!='
+ group_name = group_name[1:]
+ if address_mask:
+ operator = '!=' if exclude else '=='
+ operator = f'& {address_mask} {operator}'
+ # for bridge, change ip_name
+ if ip_name == 'bri':
+ ip_name = 'ip' if ipvx_address_group == 'ipv4_address_group' else 'ip6'
+ def_suffix = '6' if ipvx_address_group == 'ipv6_address_group' else ''
+ output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
+ for ipvx_network_group in ['network_group', 'ipv4_network_group', 'ipv6_network_group']:
+ if ipvx_network_group in group:
+ group_name = group[ipvx_network_group]
+ operator = ''
+ if group_name[0] == "!":
+ operator = '!='
+ group_name = group_name[1:]
+ # for bridge, change ip_name
+ if ip_name == 'bri':
+ ip_name = 'ip' if ipvx_network_group == 'ipv4_network_group' else 'ip6'
+ def_suffix = '6' if ipvx_network_group == 'ipv6_network_group' else ''
+ output.append(f'{ip_name} {prefix}addr {operator} @N{def_suffix}_{group_name}')
+ if 'dynamic_address_group' in group:
group_name = group['dynamic_address_group']
operator = ''
- exclude = group_name[0] == "!"
- if exclude:
+ if group_name[0] == "!":
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @DA{def_suffix}_{group_name}')
@@ -260,13 +282,6 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}')
- elif 'network_group' in group:
- group_name = group['network_group']
- operator = ''
- if group_name[0] == '!':
- operator = '!='
- group_name = group_name[1:]
- output.append(f'{ip_name} {prefix}addr {operator} @N{def_suffix}_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
@@ -473,11 +488,20 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'mark' in rule_conf['set']:
mark = rule_conf['set']['mark']
output.append(f'meta mark set {mark}')
+ if 'vrf' in rule_conf['set']:
+ set_table = True
+ vrf_name = rule_conf['set']['vrf']
+ if vrf_name == 'default':
+ table = rt_global_vrf
+ else:
+ # NOTE: VRF->table ID lookup depends on the VRF iface already existing.
+ table = get_vrf_tableid(vrf_name)
if 'table' in rule_conf['set']:
set_table = True
table = rule_conf['set']['table']
if table == 'main':
- table = '254'
+ table = rt_global_table
+ if set_table:
mark = 0x7FFFFFFF - int(table)
output.append(f'meta mark set {mark}')
if 'tcp_mss' in rule_conf['set']:
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index 9511386f4..251cbeb36 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -15,6 +15,7 @@
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
+from vyos.utils.vti_updown_db import vti_updown_db_exists, open_vti_updown_db_readonly
@Interface.register
class VTIIf(Interface):
@@ -27,6 +28,10 @@ class VTIIf(Interface):
},
}
+ def __init__(self, ifname, **kwargs):
+ self.bypass_vti_updown_db = kwargs.pop("bypass_vti_updown_db", False)
+ super().__init__(ifname, **kwargs)
+
def _create(self):
# This table represents a mapping from VyOS internal config dict to
# arguments used by iproute2. For more information please refer to:
@@ -57,8 +62,18 @@ class VTIIf(Interface):
self.set_interface('admin_state', 'down')
def set_admin_state(self, state):
- """ Handled outside by /etc/ipsec.d/vti-up-down """
- pass
+ """
+ Set interface administrative state to be 'up' or 'down'.
+
+ The interface will only be brought 'up' if ith is attached to an
+ active ipsec site-to-site connection or remote access connection.
+ """
+ if state == 'down' or self.bypass_vti_updown_db:
+ super().set_admin_state(state)
+ elif vti_updown_db_exists():
+ with open_vti_updown_db_readonly() as db:
+ if db.wantsInterfaceUp(self.ifname):
+ super().set_admin_state(state)
def get_mac(self):
""" Get a synthetic MAC address. """
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
new file mode 100644
index 000000000..b646f881e
--- /dev/null
+++ b/python/vyos/utils/serial.py
@@ -0,0 +1,118 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os, re, json
+from typing import List
+
+from vyos.base import Warning
+from vyos.utils.io import ask_yes_no
+from vyos.utils.process import cmd
+
+GLOB_GETTY_UNITS = 'serial-getty@*.service'
+RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$')
+
+SD_UNIT_PATH = '/run/systemd/system'
+UTMP_PATH = '/run/utmp'
+
+def get_serial_units(include_devices=[]):
+ # Since we cannot depend on the current config for decommissioned ports,
+ # we just grab everything that systemd knows about.
+ tmp = cmd(f'systemctl list-units {GLOB_GETTY_UNITS} --all --output json --no-pager')
+ getty_units = json.loads(tmp)
+ for sdunit in getty_units:
+ m = RE_GETTY_DEVICES.search(sdunit['unit'])
+ if m is None:
+ Warning(f'Serial console unit name "{sdunit["unit"]}" is malformed and cannot be checked for activity!')
+ continue
+
+ getty_device = m.group(1)
+ if include_devices and getty_device not in include_devices:
+ continue
+
+ sdunit['device'] = getty_device
+
+ return getty_units
+
+def get_authenticated_ports(units):
+ connected = []
+ ports = [ x['device'] for x in units if 'device' in x ]
+ #
+ # utmpdump just gives us an easily parseable dump of currently logged-in sessions, for eg:
+ # $ utmpdump /run/utmp
+ # Utmp dump of /run/utmp
+ # [2] [00000] [~~ ] [reboot ] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:56:53,958484+00:00]
+ # [1] [00051] [~~ ] [runlevel] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:57:01,790808+00:00]
+ # [6] [03178] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [2024-06-18T13:57:31,015392+00:00]
+ # [7] [37151] [ts/0] [vyos ] [pts/0 ] [10.9.8.7 ] [10.9.8.7 ] [2024-07-04T13:42:08,760892+00:00]
+ # [8] [24812] [ts/1] [ ] [pts/1 ] [10.9.8.7 ] [10.9.8.7 ] [2024-06-20T18:10:07,309365+00:00]
+ #
+ # We can safely skip blank or LOGIN sessions with valid device names.
+ #
+ for line in cmd(f'utmpdump {UTMP_PATH}').splitlines():
+ row = line.split('] [')
+ user_name = row[3].strip()
+ user_term = row[4].strip()
+ if user_name and user_name != 'LOGIN' and user_term in ports:
+ connected.append(user_term)
+
+ return connected
+
+def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]):
+ # restart_login_consoles() is called from both conf- and op-mode scripts, including
+ # the warning messages and user prompts common to both.
+ #
+ # The default case, called with no arguments, is a simple serial-getty restart &
+ # cleanup wrapper with no output or prompts that can be used from anywhere.
+ #
+ # quiet and prompt_user args have been split from an original "no_prompt", in
+ # order to support the completely silent default use case. "no_prompt" would
+ # only suppress the user interactive prompt.
+ #
+ # quiet intentionally does not suppress a vyos.base.Warning() for malformed
+ # device names in _get_serial_units().
+ #
+ cmd('systemctl daemon-reload')
+
+ units = get_serial_units(devices)
+ connected = get_authenticated_ports(units)
+
+ if connected:
+ if not quiet:
+ Warning('There are user sessions connected via serial console that '\
+ 'will be terminated when serial console settings are changed!')
+ if not prompt_user:
+ # This flag is used by conf_mode/system_console.py to reset things, if there's
+ # a problem, the user should issue a manual restart for serial-getty.
+ Warning('Please ensure all settings are committed and saved before issuing a ' \
+ '"restart serial console" command to apply new configuration!')
+ if not prompt_user:
+ return False
+ if not ask_yes_no('Any uncommitted changes from these sessions will be lost\n' \
+ 'and in-progress actions may be left in an inconsistent state.\n'\
+ '\nContinue?'):
+ return False
+
+ for unit in units:
+ if 'device' not in unit:
+ continue # malformed or filtered.
+ unit_name = unit['unit']
+ unit_device = unit['device']
+ if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)):
+ cmd(f'systemctl restart {unit_name}')
+ else:
+ # Deleted stubs don't need to be restarted, just shut them down.
+ cmd(f'systemctl stop {unit_name}')
+
+ return True
diff --git a/python/vyos/utils/vti_updown_db.py b/python/vyos/utils/vti_updown_db.py
new file mode 100644
index 000000000..b491fc6f2
--- /dev/null
+++ b/python/vyos/utils/vti_updown_db.py
@@ -0,0 +1,194 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from contextlib import contextmanager
+from syslog import syslog
+
+VTI_WANT_UP_IFLIST = '/tmp/ipsec_vti_interfaces'
+
+def vti_updown_db_exists():
+ """ Returns true if the database exists """
+ return os.path.exists(VTI_WANT_UP_IFLIST)
+
+@contextmanager
+def open_vti_updown_db_for_create_or_update():
+ """ Opens the database for reading and writing, creating the database if it does not exist """
+ if vti_updown_db_exists():
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ else:
+ f = open(VTI_WANT_UP_IFLIST, 'x+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_for_update():
+ """ Opens the database for reading and writing, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_readonly():
+ """ Opens the database for reading, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+def remove_vti_updown_db():
+ """ Brings down any interfaces referenced by the database and removes the database """
+ # We need to process the DB first to bring down any interfaces still up
+ with open_vti_updown_db_for_update() as db:
+ db.removeAllOtherInterfaces([])
+ # this usage of commit will only ever bring down interfaces,
+ # do not need to provide a functional interface dict supplier
+ db.commit(lambda _: None)
+
+ os.unlink(VTI_WANT_UP_IFLIST)
+
+class VTIUpDownDB:
+ # The VTI Up-Down DB is a text-based database of space-separated "ifspecs".
+ #
+ # ifspecs can come in one of the two following formats:
+ #
+ # persistent format: <interface name>
+ # indicates the named interface should always be up.
+ #
+ # connection format: <interface name>:<connection name>:<protocol>
+ # indicates the named interface wants to be up due to an established
+ # connection <connection name> using the <protocol> protocol.
+ #
+ # The configuration tree and ipsec daemon connection up-down hook
+ # modify this file as needed and use it to determine when a
+ # particular event or configuration change should lead to changing
+ # the interface state.
+
+ def __init__(self, f):
+ self._fileHandle = f
+ self._ifspecs = set([entry.strip() for entry in f.read().split(" ") if entry and not entry.isspace()])
+ self._ifsUp = set()
+ self._ifsDown = set()
+
+ def add(self, interface, connection = None, protocol = None):
+ """
+ Adds a new entry to the DB.
+
+ If an interface name, connection name, and protocol are supplied,
+ creates a connection entry.
+
+ If only an interface name is specified, creates a persistent entry
+ for the given interface.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec not in self._ifspecs:
+ self._ifspecs.add(ifspec)
+ self._ifsUp.add(interface)
+ self._ifsDown.discard(interface)
+
+ def remove(self, interface, connection = None, protocol = None):
+ """
+ Removes a matching entry from the DB.
+
+ If no matching entry can be fonud, the operation returns successfully.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec in self._ifspecs:
+ self._ifspecs.remove(ifspec)
+ interface_remains = False
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ interface_remains = True
+
+ if not interface_remains:
+ self._ifsDown.add(interface)
+ self._ifsUp.discard(interface)
+
+ def wantsInterfaceUp(self, interface):
+ """ Returns whether the DB contains at least one entry referencing the given interface """
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ return True
+
+ return False
+
+ def removeAllOtherInterfaces(self, interface_list):
+ """ Removes all interfaces not included in the given list from the DB """
+ updated_ifspecs = set([ifspec for ifspec in self._ifspecs if ifspec.split(':')[0] in interface_list])
+ removed_ifspecs = self._ifspecs - updated_ifspecs
+ self._ifspecs = updated_ifspecs
+ interfaces_to_bring_down = [ifspec.split(':')[0] for ifspec in removed_ifspecs]
+ self._ifsDown.update(interfaces_to_bring_down)
+ self._ifsUp.difference_update(interfaces_to_bring_down)
+
+ def setPersistentInterfaces(self, interface_list):
+ """ Updates the set of persistently up interfaces to match the given list """
+ new_presistent_interfaces = set(interface_list)
+ current_presistent_interfaces = set([ifspec for ifspec in self._ifspecs if ':' not in ifspec])
+ added_presistent_interfaces = new_presistent_interfaces - current_presistent_interfaces
+ removed_presistent_interfaces = current_presistent_interfaces - new_presistent_interfaces
+
+ for interface in added_presistent_interfaces:
+ self.add(interface)
+
+ for interface in removed_presistent_interfaces:
+ self.remove(interface)
+
+ def commit(self, interface_dict_supplier):
+ """
+ Writes the DB to disk and brings interfaces up and down as needed.
+
+ Only interfaces referenced by entries modified in this DB session
+ are manipulated. If an interface is called to be brought up, the
+ provided interface_config_supplier function is invoked and expected
+ to return the config dictionary for the interface.
+ """
+ from vyos.ifconfig import VTIIf
+ from vyos.utils.process import call
+ from vyos.utils.network import get_interface_config
+
+ self._fileHandle.seek(0)
+ self._fileHandle.write(' '.join(self._ifspecs))
+ self._fileHandle.truncate()
+
+ for interface in self._ifsDown:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if vti_link_up:
+ call(f'sudo ip link set {interface} down')
+ syslog(f'Interface {interface} is admin down ...')
+
+ self._ifsDown.clear()
+
+ for interface in self._ifsUp:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if not vti_link_up:
+ vti = interface_dict_supplier(interface)
+ if 'disable' not in vti:
+ tmp = VTIIf(interface, bypass_vti_updown_db = True)
+ tmp.update(vti)
+ syslog(f'Interface {interface} is admin up ...')
+
+ self._ifsUp.clear()
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index e6317050c..551f8ce65 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -695,13 +695,21 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
self.verify_nftables_chain([['return']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
- def test_bridge_basic_rules(self):
+ def test_bridge_firewall(self):
name = 'smoketest'
interface_in = 'eth0'
mac_address = '00:53:00:00:00:01'
vlan_id = '12'
vlan_prior = '3'
+ # Check bridge-nf-call-iptables default value: 0
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '0')
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
+
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', 'AGV6', 'address', '2001:db1::1'])
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
+ self.cli_set(['firewall', 'global-options', 'apply-to-bridged-traffic', 'ipv4'])
+
self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept'])
self.cli_set(['firewall', 'bridge', 'name', name, 'default-log'])
self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept'])
@@ -718,20 +726,41 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name])
self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'inbound-interface', 'name', interface_in])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'source', 'address', '192.0.2.2'])
+ self.cli_set(['firewall', 'bridge', 'input', 'filter', 'rule', '1', 'state', 'new'])
+
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'action', 'notrack'])
+ self.cli_set(['firewall', 'bridge', 'prerouting', 'filter', 'rule', '1', 'destination', 'group', 'ipv6-address-group', 'AGV6'])
+
self.cli_commit()
nftables_search = [
+ ['set A6_AGV6'],
+ ['type ipv6_addr'],
+ ['elements', '2001:db1::1'],
['chain VYOS_FORWARD_filter'],
['type filter hook forward priority filter; policy accept;'],
+ ['jump VYOS_STATE_POLICY'],
[f'vlan id {vlan_id}', 'accept'],
[f'vlan pcp {vlan_prior}', f'jump NAME_{name}'],
['log prefix "[bri-FWD-filter-default-D]"', 'drop', 'FWD-filter default-action drop'],
[f'chain NAME_{name}'],
[f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'],
- ['accept', f'{name} default-action accept']
+ ['accept', f'{name} default-action accept'],
+ ['chain VYOS_INPUT_filter'],
+ ['type filter hook input priority filter; policy accept;'],
+ ['ct state new', 'ip saddr 192.0.2.2', f'iifname "{interface_in}"', 'accept'],
+ ['chain VYOS_PREROUTING_filter'],
+ ['type filter hook prerouting priority filter; policy accept;'],
+ ['ip6 daddr @A6_AGV6', 'notrack']
]
self.verify_nftables(nftables_search, 'bridge vyos_filter')
+ ## Check bridge-nf-call-iptables is set to 1, and for ipv6 remains on default 0
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-iptables'), '1')
+ self.assertEqual(get_sysctl('net.bridge.bridge-nf-call-ip6tables'), '0')
def test_source_validation(self):
# Strict
diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py
index 871ac650b..8d90ca5ad 100755
--- a/smoketest/scripts/cli/test_interfaces_vti.py
+++ b/smoketest/scripts/cli/test_interfaces_vti.py
@@ -39,7 +39,8 @@ class VTIInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
- # VTI interface are always down and only brought up by IPSec
+ # VTI interfaces are default down and only brought up when an
+ # IPSec connection is configured to use them
for intf in self._interfaces:
self.assertTrue(is_intf_addr_assigned(intf, addr))
self.assertEqual(Interface(intf).get_admin_state(), 'down')
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 462fc24d0..797ab9770 100755
--- a/smoketest/scripts/cli/test_policy_route.py
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -25,6 +25,8 @@ conn_mark = '555'
conn_mark_set = '111'
table_mark_offset = 0x7fffffff
table_id = '101'
+vrf = 'PBRVRF'
+vrf_table_id = '102'
interface = 'eth0'
interface_wc = 'ppp*'
interface_ip = '172.16.10.1/24'
@@ -39,11 +41,14 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_delete(cls, ['protocols', 'static', 'table', table_id])
+ cls.cli_delete(cls, ['vrf', 'name', vrf])
super(TestPolicyRoute, cls).tearDownClass()
@@ -180,6 +185,50 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
self.verify_rules(ip_rule_search)
+ def test_pbr_vrf(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'vrf', vrf])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'vrf', vrf])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(vrf_table_id))
+
+ # IPv4
+
+ nftables_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR_UD_smoketest'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+
+ nftables6_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+ # IP rule fwmark -> table
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(vrf_table_id)), 'lookup ' + vrf]
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
def test_pbr_matching_criteria(self):
self.cli_set(['policy', 'route', 'smoketest', 'default-log'])
self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'udp'])
diff --git a/smoketest/scripts/cli/test_protocols_static_multicast.py b/smoketest/scripts/cli/test_protocols_static_multicast.py
new file mode 100755
index 000000000..9fdda236f
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_static_multicast.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+
+base_path = ['protocols', 'static', 'multicast']
+
+
+class TestProtocolsStaticMulticast(VyOSUnitTestSHIM.TestCase):
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ mroute = self.getFRRconfig('ip mroute', end='')
+ self.assertFalse(mroute)
+
+ def test_01_static_multicast(self):
+
+ self.cli_set(base_path + ['route', '224.202.0.0/24', 'next-hop', '224.203.0.1'])
+ self.cli_set(base_path + ['interface-route', '224.203.0.0/24', 'next-hop-interface', 'eth0'])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig('ip mroute', end='')
+
+ self.assertIn('ip mroute 224.202.0.0/24 224.203.0.1', frrconfig)
+ self.assertIn('ip mroute 224.203.0.0/24 eth0', frrconfig)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 2dc66485b..3b8687b93 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -20,6 +20,7 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Interface
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
@@ -140,6 +141,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_delete(tunnel_path)
+ self.cli_delete(vti_path)
self.cli_commit()
# Check for no longer running process
@@ -342,6 +344,12 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
for line in swanctl_secrets_lines:
self.assertRegex(swanctl_conf, fr'{line}')
+ # Site-to-site interfaces should start out as 'down'
+ self.assertEqual(Interface(vti).get_admin_state(), 'down')
+
+ # Disable PKI
+ self.tearDownPKI()
+
def test_dmvpn(self):
tunnel_if = 'tun100'
@@ -478,9 +486,6 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
- # There is only one VTI test so no need to delete this globally in tearDown()
- self.cli_delete(vti_path)
-
# Disable PKI
self.tearDownPKI()
@@ -1086,5 +1091,269 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.tearDownPKI()
+ def test_remote_access_pool_range(self):
+ # Same as test_remote_access but using an IP pool range instead of prefix
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ range_start = '172.16.250.2'
+ range_stop = '172.16.250.254'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'start', range_start])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'stop', range_stop])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'auth = eap-mschapv2',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'replay_window = 32',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'eap-{conn_name}-{username}',
+ f'secret = "{password}"',
+ f'id-{conn_name}-{username} = "{username}"',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {range_start}-{range_stop}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
+ def test_remote_access_vti(self):
+ # Set up and use a VTI interface for the remote access VPN
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ vti = 'vti10'
+ ip_pool_name = 'ra-rw-ipv4'
+ username = 'vyos'
+ password = 'secret'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['10.1.1.1']
+ range_start = '10.1.1.10'
+ range_stop = '10.1.1.254'
+
+ # VTI interface
+ self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24'])
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'bind', vti])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'start', range_start])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'range', 'stop', range_stop])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+
+ if_id = vti.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
+
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = {ike_lifetime}s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'auth = eap-mschapv2',
+ f'eap_id = %any',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'dpd_action = clear',
+ f'replay_window = 32',
+ f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
+ f'if_id_out = {if_id}',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_secrets_lines = [
+ f'eap-{conn_name}-{username}',
+ f'secret = "{password}"',
+ f'id-{conn_name}-{username} = "{username}"',
+ ]
+ for line in swanctl_secrets_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {range_start}-{range_stop}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ # Remote access interfaces should be set to 'up' during configure
+ self.assertEqual(Interface(vti).get_admin_state(), 'up')
+
+ # Delete the connection to verify the VTI interfaces is taken down
+ self.cli_delete(base_path + ['remote-access', 'connection', conn_name])
+ self.cli_commit()
+ self.assertEqual(Interface(vti).get_admin_state(), 'down')
+
+ self.tearDownPKI()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index 176882ca5..2bb6c91c1 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -19,6 +19,8 @@ import os
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+from json import loads
+from jmespath import search
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
@@ -28,6 +30,7 @@ from vyos.utils.network import get_interface_config
from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import interface_exists
+from vyos.utils.process import cmd
from vyos.utils.system import sysctl_read
base_path = ['vrf']
@@ -557,26 +560,39 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig)
def test_vrf_conntrack(self):
- table = '1000'
+ table = '8710'
nftables_rules = {
'vrf_zones_ct_in': ['ct original zone set iifname map @ct_iface_map'],
'vrf_zones_ct_out': ['ct original zone set oifname map @ct_iface_map']
}
- self.cli_set(base_path + ['name', 'blue', 'table', table])
+ self.cli_set(base_path + ['name', 'randomVRF', 'table', '1000'])
self.cli_commit()
# Conntrack rules should not be present
for chain, rule in nftables_rules.items():
self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=True)
+ # conntrack is only enabled once NAT, NAT66 or firewalling is enabled
self.cli_set(['nat'])
- self.cli_commit()
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ table = str(int(table) + 1)
+ # We need the commit inside the loop to trigger the bug in T6603
+ self.cli_commit()
# Conntrack rules should now be present
for chain, rule in nftables_rules.items():
self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=False)
+ # T6603: there should be only ONE entry for the iifname/oifname in the chains
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ # ['vrf_zones_ct_in', 'vrf_zones_ct_out']
+ self.assertEqual(num_rules, 2)
+
self.cli_delete(['nat'])
if __name__ == '__main__':
diff --git a/src/completion/list_login_ttys.py b/src/completion/list_login_ttys.py
new file mode 100644
index 000000000..4d77a1b8b
--- /dev/null
+++ b/src/completion/list_login_ttys.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.utils.serial import get_serial_units
+
+if __name__ == '__main__':
+ # Autocomplete uses runtime state rather than the config tree, as a manual
+ # restart/cleanup may be needed for deleted devices.
+ tty_completions = [ '<text>' ] + [ x['device'] for x in get_serial_units() if 'device' in x ]
+ print(' '.join(tty_completions))
+
+
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 352d5cbb1..02bf00bcc 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -36,6 +36,7 @@ from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
from vyos import ConfigError
from vyos import airbag
+from subprocess import run as subp_run
airbag.enable()
@@ -47,7 +48,12 @@ valid_groups = [
'domain_group',
'network_group',
'port_group',
- 'interface_group'
+ 'interface_group',
+ ## Added for group ussage in bridge firewall
+ 'ipv4_address_group',
+ 'ipv6_address_group',
+ 'ipv4_network_group',
+ 'ipv6_network_group'
]
nested_group_types = [
@@ -128,41 +134,32 @@ def get_config(config=None):
return firewall
-def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False):
+def verify_jump_target(firewall, hook, jump_target, family, recursive=False):
targets_seen = []
targets_pending = [jump_target]
while targets_pending:
target = targets_pending.pop()
- if not ipv6:
- if target not in dict_search_args(firewall, 'ipv4', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
- target_rules = dict_search_args(firewall, 'ipv4', 'name', target, 'rule')
- else:
- if target not in dict_search_args(firewall, 'ipv6', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
- target_rules = dict_search_args(firewall, 'ipv6', 'name', target, 'rule')
+ if 'name' not in firewall[family]:
+ raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system')
+ elif target not in dict_search_args(firewall, family, 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system')
- no_ipsec_in = root_chain in ('output', )
+ target_rules = dict_search_args(firewall, family, 'name', target, 'rule')
+ no_ipsec_in = hook in ('output', )
if target_rules:
for target_rule_conf in target_rules.values():
# Output hook types will not tolerate 'meta ipsec exists' matches even in jump targets:
if no_ipsec_in and (dict_search_args(target_rule_conf, 'ipsec', 'match_ipsec_in') is not None \
or dict_search_args(target_rule_conf, 'ipsec', 'match_none_in') is not None):
- if not ipv6:
- raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall name {target} rules contain incompatible ipsec inbound matches')
- else:
- raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall ipv6 name {target} rules contain incompatible ipsec inbound matches')
+ raise ConfigError(f'Invalid jump-target for {hook}. Firewall {family} name {target} rules contain incompatible ipsec inbound matches')
# Make sure we're not looping back on ourselves somewhere:
if recursive and 'jump_target' in target_rule_conf:
child_target = target_rule_conf['jump_target']
if child_target in targets_seen:
- if not ipv6:
- raise ConfigError(f'Loop detected in jump-targets, firewall name {target} refers to previously traversed name {child_target}')
- else:
- raise ConfigError(f'Loop detected in jump-targets, firewall ipv6 name {target} refers to previously traversed ipv6 name {child_target}')
+ raise ConfigError(f'Loop detected in jump-targets, firewall {family} name {target} refers to previously traversed {family} name {child_target}')
targets_pending.append(child_target)
if len(targets_seen) == 7:
path_txt = ' -> '.join(targets_seen)
@@ -170,7 +167,7 @@ def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False)
targets_seen.append(target)
-def verify_rule(firewall, chain_name, rule_conf, ipv6):
+def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if 'action' not in rule_conf:
raise ConfigError('Rule action must be defined')
@@ -181,10 +178,10 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
if 'jump' not in rule_conf['action']:
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
- if chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code.
- verify_jump_target(firewall, chain_name, target, ipv6, recursive=True)
+ if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ verify_jump_target(firewall, hook, target, family, recursive=True)
else:
- verify_jump_target(firewall, chain_name, target, ipv6, recursive=False)
+ verify_jump_target(firewall, hook, target, family, recursive=False)
if rule_conf['action'] == 'offload':
if 'offload_target' not in rule_conf:
@@ -246,9 +243,9 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
raise ConfigError(f'Cannot match a tcp flag as set and not set')
if 'protocol' in rule_conf:
- if rule_conf['protocol'] == 'icmp' and ipv6:
+ if rule_conf['protocol'] == 'icmp' and family == 'ipv6':
raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp')
- if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6:
+ if rule_conf['protocol'] == 'ipv6-icmp' and family == 'ipv4':
raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp')
for side in ['destination', 'source']:
@@ -266,7 +263,18 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
if group in side_conf['group']:
group_name = side_conf['group'][group]
- fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ if family == 'ipv6' and group in ['address_group', 'network_group']:
+ fw_group = f'ipv6_{group}'
+ elif family == 'bridge':
+ if group =='ipv4_address_group':
+ fw_group = 'address_group'
+ elif group == 'ipv4_network_group':
+ fw_group = 'network_group'
+ else:
+ fw_group = group
+ else:
+ fw_group = group
+
error_group = fw_group.replace("_", "-")
if group in ['address_group', 'network_group', 'domain_group']:
@@ -302,7 +310,7 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6):
raise ConfigError(f'Dynamic address group must be defined.')
else:
target = rule_conf['add_address_to_group'][type]['address_group']
- fwall_group = 'ipv6_address_group' if ipv6 else 'address_group'
+ fwall_group = 'ipv6_address_group' if family == 'ipv6' else 'address_group'
group_obj = dict_search_args(firewall, 'group', 'dynamic_group', fwall_group, target)
if group_obj is None:
raise ConfigError(f'Invalid dynamic address group on firewall rule')
@@ -379,43 +387,25 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
- if 'ipv4' in firewall:
- for name in ['name','forward','input','output', 'prerouting']:
- if name in firewall['ipv4']:
- for name_id, name_conf in firewall['ipv4'][name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
- if name_conf['default_jump_target'] == name_id:
- raise ConfigError(f'Loop detected on default-jump-target.')
- verify_jump_target(firewall, name, target, False, recursive=True)
-
- if 'rule' in name_conf:
- for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, name, rule_conf, False)
-
- if 'ipv6' in firewall:
- for name in ['name','forward','input','output', 'prerouting']:
- if name in firewall['ipv6']:
- for name_id, name_conf in firewall['ipv6'][name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
- if name_conf['default_jump_target'] == name_id:
- raise ConfigError(f'Loop detected on default-jump-target.')
- verify_jump_target(firewall, name, target, True, recursive=True)
-
- if 'rule' in name_conf:
- for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, name, rule_conf, True)
-
- #### ZONESSSS
+ for family in ['ipv4', 'ipv6', 'bridge']:
+ if family in firewall:
+ for chain in ['name','forward','input','output', 'prerouting']:
+ if chain in firewall[family]:
+ for priority, priority_conf in firewall[family][chain].items():
+ if 'jump' in priority_conf['default_action'] and 'default_jump_target' not in priority_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in priority_conf:
+ target = priority_conf['default_jump_target']
+ if 'jump' not in priority_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if priority_conf['default_jump_target'] == priority:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ if target not in dict_search_args(firewall[family], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ if 'rule' in priority_conf:
+ for rule_id, rule_conf in priority_conf['rule'].items():
+ verify_rule(firewall, family, chain, priority, rule_id, rule_conf)
+
local_zone = False
zone_interfaces = []
@@ -495,8 +485,53 @@ def generate(firewall):
render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall)
return None
+def parse_firewall_error(output):
+ # Define the regex patterns to extract the error message and the comment
+ error_pattern = re.compile(r'Error:\s*(.*?)\n')
+ comment_pattern = re.compile(r'comment\s+"([^"]+)"')
+ error_output = []
+
+ # Find all error messages in the output
+ error_matches = error_pattern.findall(output)
+ # Find all comment matches in the output
+ comment_matches = comment_pattern.findall(output)
+
+ if not error_matches or not comment_matches:
+ raise ConfigError(f'Unknown firewall error detected: {output}')
+
+ error_output.append('Fail to apply firewall')
+ # Loop over the matches and process them
+ for error_message, comment in zip(error_matches, comment_matches):
+ # Parse the comment
+ parsed_entries = comment.split('-')
+ family = 'bridge' if parsed_entries[0] == 'bri' else parsed_entries[0]
+ if parsed_entries[1] == 'NAM':
+ chain = 'name'
+ elif parsed_entries[1] == 'FWD':
+ chain = 'forward'
+ elif parsed_entries[1] == 'INP':
+ chain = 'input'
+ elif parsed_entries[1] == 'OUT':
+ chain = 'output'
+ elif parsed_entries[1] == 'PRE':
+ chain = 'prerouting'
+ error_output.append(f'Error found on: firewall {family} {chain} {parsed_entries[2]} rule {parsed_entries[3]}')
+ error_output.append(f'\tError message: {error_message.strip()}')
+
+ raise ConfigError('\n'.join(error_output))
+
def apply(firewall):
+ # Use nft -c option to check current configuration file
+ completed_process = subp_run(['nft', '-c', '--file', nftables_conf], capture_output=True)
+ install_result = completed_process.returncode
+ if install_result == 1:
+ # We need to handle firewall error
+ output = completed_process.stderr
+ parse_firewall_error(output.decode())
+
+ # No error detected during check, we can apply the new configuration
install_result, output = rc_cmd(f'nft --file {nftables_conf}')
+ # Double check just in case
if install_result == 1:
raise ConfigError(f'Failed to apply firewall: {output}')
diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py
index b9f827bee..f0a70436e 100755
--- a/src/conf_mode/interfaces_l2tpv3.py
+++ b/src/conf_mode/interfaces_l2tpv3.py
@@ -86,6 +86,8 @@ def generate(l2tpv3):
return None
def apply(l2tpv3):
+ check_kmod(k_mod)
+
# Check if L2TPv3 interface already exists
if interface_exists(l2tpv3['ifname']):
# L2TPv3 is picky when changing tunnels/sessions, thus we can simply
@@ -102,7 +104,6 @@ def apply(l2tpv3):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index 0e0b77877..7abdfdbfa 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -103,7 +103,12 @@ def verify(wireguard):
public_keys.append(peer['public_key'])
+def generate(wireguard):
+ return None
+
def apply(wireguard):
+ check_kmod('wireguard')
+
if 'rebuild_required' in wireguard or 'deleted' in wireguard:
wg = WireGuardIf(**wireguard)
# WireGuard only supports peer removal based on the configured public-key,
@@ -123,7 +128,6 @@ def apply(wireguard):
if __name__ == '__main__':
try:
- check_kmod('wireguard')
c = get_config()
verify(c)
apply(c)
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index f35a250cb..d24675ee6 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -239,6 +239,8 @@ def verify(wifi):
return None
def generate(wifi):
+ check_kmod('mac80211')
+
interface = wifi['ifname']
# Delete config files if interface is removed
@@ -333,7 +335,6 @@ def apply(wifi):
if __name__ == '__main__':
try:
- check_kmod('mac80211')
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index f74bb217e..39803fa02 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -240,6 +240,8 @@ def generate(nat):
return None
def apply(nat):
+ check_kmod(k_mod)
+
cmd(f'nft --file {nftables_nat_config}')
cmd(f'nft --file {nftables_static_nat_conf}')
@@ -253,7 +255,6 @@ def apply(nat):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py
index 32a1c47d1..df501ce7f 100755
--- a/src/conf_mode/nat64.py
+++ b/src/conf_mode/nat64.py
@@ -46,7 +46,12 @@ def get_config(config: Config | None = None) -> None:
base = ["nat64"]
nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True)
- base_src = base + ["source", "rule"]
+ return nat64
+
+
+def verify(nat64) -> None:
+ check_kmod(["jool"])
+ base_src = ["nat64", "source", "rule"]
# Load in existing instances so we can destroy any unknown
lines = cmd("jool instance display --csv").splitlines()
@@ -76,12 +81,8 @@ def get_config(config: Config | None = None) -> None:
):
rules[num]["recreate"] = True
- return nat64
-
-
-def verify(nat64) -> None:
if not nat64:
- # no need to verify the CLI as nat64 is going to be deactivated
+ # nothing left to do
return
if dict_search("source.rule", nat64):
@@ -128,6 +129,9 @@ def verify(nat64) -> None:
def generate(nat64) -> None:
+ if not nat64:
+ return
+
os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
if dict_search("source.rule", nat64):
@@ -184,6 +188,7 @@ def generate(nat64) -> None:
def apply(nat64) -> None:
if not nat64:
+ unload_kmod(['jool'])
return
if dict_search("source.rule", nat64):
@@ -211,7 +216,6 @@ def apply(nat64) -> None:
if __name__ == "__main__":
try:
- check_kmod(["jool"])
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index 075738dad..c44320f36 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -112,6 +112,8 @@ def apply(nat):
if not nat:
return None
+ check_kmod(k_mod)
+
cmd(f'nft --file {nftables_nat66_config}')
call_dependents()
@@ -119,7 +121,6 @@ def apply(nat):
if __name__ == '__main__':
try:
- check_kmod(k_mod)
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index c58fe1bce..223175b8a 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -25,6 +25,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search_args
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -83,6 +86,9 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
if not tcp_flags or 'syn' not in tcp_flags:
raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+ if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF')
+
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
if dict_search_args(rule_conf, 'protocol') != 'tcp':
@@ -152,15 +158,26 @@ def apply_table_marks(policy):
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
for rule_id, rule_conf in pol_conf['rule'].items():
+ vrf_table_id = None
set_table = dict_search_args(rule_conf, 'set', 'table')
- if set_table:
+ set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
+ if set_vrf:
+ if set_vrf == 'default':
+ vrf_table_id = rt_global_vrf
+ else:
+ vrf_table_id = get_vrf_tableid(set_vrf)
+ elif set_table:
if set_table == 'main':
- set_table = '254'
- if set_table in tables:
+ vrf_table_id = rt_global_table
+ else:
+ vrf_table_id = set_table
+ if vrf_table_id is not None:
+ vrf_table_id = int(vrf_table_id)
+ if vrf_table_id in tables:
continue
- tables.append(set_table)
- table_mark = mark_offset - int(set_table)
- cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
+ tables.append(vrf_table_id)
+ table_mark = mark_offset - vrf_table_id
+ cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}')
def cleanup_table_marks():
for cmd_str in ['ip', 'ip -6']:
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 7f6ae3680..d323ceb4f 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,15 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
from ipaddress import IPv4Address
from sys import exit
from vyos import ConfigError
+from vyos import frr
from vyos.config import Config
-from vyos.utils.process import call
-from vyos.template import render
+from vyos.template import render_to_string
from vyos import airbag
airbag.enable()
@@ -92,23 +91,39 @@ def verify(mroute):
if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
raise ConfigError(route + " not a multicast network")
+
def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr/static_mcast.frr.j2', mroute)
+ mroute['new_frr_config'] = render_to_string('frr/static_mcast.frr.j2', mroute)
return None
+
def apply(mroute):
if mroute is None:
return None
+ static_daemon = 'staticd'
+
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(static_daemon)
- if os.path.exists(config_file):
- call(f'vtysh -d staticd -f {config_file}')
- os.remove(config_file)
+ if 'old_mroute' in mroute:
+ for route_gr in mroute['old_mroute']:
+ for nh in mroute['old_mroute'][route_gr]:
+ if mroute['old_mroute'][route_gr][nh]:
+ frr_cfg.modify_section(f'^ip mroute {route_gr} {nh} {mroute["old_mroute"][route_gr][nh]}')
+ else:
+ frr_cfg.modify_section(f'^ip mroute {route_gr} {nh}')
+
+ if 'new_frr_config' in mroute:
+ frr_cfg.add_before(frr.default_add_before, mroute['new_frr_config'])
+
+ frr_cfg.commit_configuration(static_daemon)
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_acceleration.py b/src/conf_mode/system_acceleration.py
index e4b248675..d2cf44ff0 100755
--- a/src/conf_mode/system_acceleration.py
+++ b/src/conf_mode/system_acceleration.py
@@ -79,6 +79,9 @@ def verify(qat):
if not data:
raise ConfigError('No QAT acceleration device found')
+def generate(qat):
+ return
+
def apply(qat):
# Shutdown VPN service which can use QAT
if 'ipsec' in qat:
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 19bbb8875..b380e0521 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -19,6 +19,7 @@ from pathlib import Path
from vyos.config import Config
from vyos.utils.process import call
+from vyos.utils.serial import restart_login_consoles
from vyos.system import grub_util
from vyos.template import render
from vyos import ConfigError
@@ -74,7 +75,6 @@ def generate(console):
for root, dirs, files in os.walk(base_dir):
for basename in files:
if 'serial-getty' in basename:
- call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
if not console or 'device' not in console:
@@ -122,6 +122,11 @@ def apply(console):
# Reload systemd manager configuration
call('systemctl daemon-reload')
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message on completing
+ # the process, but not halt configuration processing with an interactive prompt.
+ restart_login_consoles(prompt_user=False, quiet=False)
+
if not console:
return None
@@ -129,13 +134,6 @@ def apply(console):
# Configure screen blank powersaving on VGA console
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
- # Start getty process on configured serial interfaces
- for device in console['device']:
- # Only start console if it exists on the running system. If a user
- # detaches a USB serial console and reboots - it should not fail!
- if os.path.exists(f'/dev/{device}'):
- call(f'systemctl restart serial-getty@{device}.service')
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index cf82b767f..b3e05a814 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -21,12 +21,15 @@ import jmespath
from sys import exit
from time import sleep
+from ipaddress import ip_address
+from netaddr import IPNetwork
+from netaddr import IPRange
-from vyos.base import Warning
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
from vyos.configdep import call_dependents
+from vyos.configdict import get_interface_dict
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
@@ -47,6 +50,9 @@ from vyos.utils.network import interface_exists
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.process import call
+from vyos.utils.vti_updown_db import vti_updown_db_exists
+from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update
+from vyos.utils.vti_updown_db import remove_vti_updown_db
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -104,6 +110,8 @@ def get_config(config=None):
ipsec = config_dict_merge(default_values, ipsec)
ipsec['dhcp_interfaces'] = set()
+ ipsec['enabled_vti_interfaces'] = set()
+ ipsec['persistent_vti_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
@@ -121,6 +129,28 @@ def get_config(config=None):
ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
+ # Collect the interface dicts for any refernced VTI interfaces in
+ # case we need to bring the interface up
+ ipsec['vti_interface_dicts'] = {}
+
+ if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
+ for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+ if 'vti' in peer_conf:
+ if 'bind' in peer_conf['vti']:
+ vti_interface = peer_conf['vti']['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
+ if 'remote_access' in ipsec:
+ if 'connection' in ipsec['remote_access']:
+ for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
return ipsec
def get_dhcp_address(iface):
@@ -249,7 +279,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in ra_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -304,6 +335,16 @@ def verify(ipsec):
if dict_search('remote_access.radius.server', ipsec) == None:
raise ConfigError('RADIUS authentication requires at least one server')
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if not interface_exists(vti_interface):
+ raise ConfigError(f'VTI interface {vti_interface} for remote-access connection {name} does not exist!')
+
+ if 'disable' not in ra_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
+ # remote access VPN interfaces are always up regardless of whether clients are connected
+ ipsec['persistent_vti_interfaces'].add(vti_interface)
+
if 'pool' in ra_conf:
if {'dhcp', 'radius'} <= set(ra_conf['pool']):
raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\
@@ -330,26 +371,73 @@ def verify(ipsec):
raise ConfigError(f'Requested pool "{pool}" does not exist!')
if 'pool' in ipsec['remote_access']:
+ pool_networks = []
for pool, pool_config in ipsec['remote_access']['pool'].items():
- if 'prefix' not in pool_config:
- raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
+ if 'prefix' not in pool_config and 'range' not in pool_config:
+ raise ConfigError(f'Mandatory prefix or range must be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config and 'range' in pool_config:
+ raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config:
+ range_is_ipv4 = is_ipv4(pool_config['prefix'])
+ range_is_ipv6 = is_ipv6(pool_config['prefix'])
+
+ net = IPNetwork(pool_config['prefix'])
+ start = net.first
+ stop = net.last
+ for network in pool_networks:
+ if start in network or stop in network:
+ raise ConfigError(f'Prefix for pool "{pool}" is already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
+
+ if 'range' in pool_config:
+ range_config = pool_config['range']
+ if not {'start', 'stop'} <= set(range_config.keys()):
+ raise ConfigError(f'Range start and stop address must be defined for pool "{pool}"!')
+
+ range_both_ipv4 = is_ipv4(range_config['start']) and is_ipv4(range_config['stop'])
+ range_both_ipv6 = is_ipv6(range_config['start']) and is_ipv6(range_config['stop'])
+
+ if not (range_both_ipv4 or range_both_ipv6):
+ raise ConfigError(f'Range start and stop must be of the same address family for pool "{pool}"!')
+
+ if ip_address(range_config['stop']) < ip_address(range_config['start']):
+ raise ConfigError(f'Range stop address must be greater or equal\n' \
+ 'to the range\'s start address for pool "{pool}"!')
+
+ range_is_ipv4 = is_ipv4(range_config['start'])
+ range_is_ipv6 = is_ipv6(range_config['start'])
+
+ start = range_config['start']
+ stop = range_config['stop']
+ for network in pool_networks:
+ if start in network:
+ raise ConfigError(f'Range "{range}" start address "{start}" already part of another pool\'s range!')
+ if stop in network:
+ raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
if 'name_server' in pool_config:
if len(pool_config['name_server']) > 2:
raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
for ns in pool_config['name_server']:
- v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_ns = is_ipv4(ns) and not range_is_ipv4
+ v6_addr_and_ns = is_ipv6(ns) and not range_is_ipv6
if v4_addr_and_ns or v6_addr_and_ns:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and name-server addresses!')
if 'exclude' in pool_config:
for exclude in pool_config['exclude']:
- v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_exclude = is_ipv4(exclude) and not range_is_ipv4
+ v6_addr_and_exclude = is_ipv6(exclude) and not range_is_ipv6
if v4_addr_and_exclude or v6_addr_and_exclude:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and exclude prefixes!')
if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
for server, server_config in ipsec['remote_access']['radius']['server'].items():
@@ -420,7 +508,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in peer_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -438,14 +527,12 @@ def verify(ipsec):
if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
- if dict_search('options.disable_route_autoinstall',
- ipsec) == None:
- Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]')
-
if 'bind' in peer_conf['vti']:
vti_interface = peer_conf['vti']['bind']
if not interface_exists(vti_interface):
raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
+ if 'disable' not in peer_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
if 'vti' not in peer_conf and 'tunnel' not in peer_conf:
raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}")
@@ -623,9 +710,21 @@ def apply(ipsec):
systemd_service = 'strongswan.service'
if not ipsec:
call(f'systemctl stop {systemd_service}')
+
+ if vti_updown_db_exists():
+ remove_vti_updown_db()
+
else:
call(f'systemctl reload-or-restart {systemd_service}')
+ if ipsec['enabled_vti_interfaces']:
+ with open_vti_updown_db_for_create_or_update() as db:
+ db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
+ db.setPersistentInterfaces(ipsec['persistent_vti_interfaces'])
+ db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
+ elif vti_updown_db_exists():
+ remove_vti_updown_db()
+
if ipsec.get('nhrp_exists', False):
try:
call_dependents()
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 184725573..72b178c89 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from jmespath import search
from json import loads
from vyos.config import Config
@@ -70,6 +71,14 @@ def has_rule(af : str, priority : int, table : str=None):
return True
return False
+def is_nft_vrf_zone_rule_setup() -> bool:
+ """
+ Check if an nftables connection tracking rule already exists
+ """
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ return bool(num_rules)
+
def vrf_interfaces(c, match):
matched = []
old_level = c.get_level()
@@ -264,6 +273,7 @@ def apply(vrf):
if not has_rule(afi, 2000, 'l3mdev'):
call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
+ nft_vrf_zone_rule_setup = False
for name, config in vrf['name'].items():
table = config['table']
if not interface_exists(name):
@@ -302,7 +312,12 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- if vrf['conntrack']:
+ # Only call into nftables as long as there is nothing setup to avoid wasting
+ # CPU time and thus lenghten the commit process
+ if not nft_vrf_zone_rule_setup:
+ nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup()
+ # Install nftables conntrack rules only once
+ if vrf['conntrack'] and not nft_vrf_zone_rule_setup:
for chain, rule in nftables_rules.items():
cmd(f'nft add rule inet vrf_zones {chain} {rule}')
diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down
index 01e9543c9..e1765ae85 100755
--- a/src/etc/ipsec.d/vti-up-down
+++ b/src/etc/ipsec.d/vti-up-down
@@ -27,40 +27,41 @@ from syslog import LOG_INFO
from vyos.configquery import ConfigTreeQuery
from vyos.configdict import get_interface_dict
-from vyos.ifconfig import VTIIf
+from vyos.utils.commit import wait_for_commit_lock
from vyos.utils.process import call
-from vyos.utils.network import get_interface_config
+from vyos.utils.vti_updown_db import open_vti_updown_db_for_update
+
+def supply_interface_dict(interface):
+ # Lazy-load the running config on first invocation
+ try:
+ conf = supply_interface_dict.cached_config
+ except AttributeError:
+ conf = ConfigTreeQuery()
+ supply_interface_dict.cached_config = conf
+
+ _, vti = get_interface_dict(conf.config, ['interfaces', 'vti'], interface)
+ return vti
if __name__ == '__main__':
verb = os.getenv('PLUTO_VERB')
connection = os.getenv('PLUTO_CONNECTION')
interface = sys.argv[1]
+ if verb.endswith('-v6'):
+ protocol = 'v6'
+ else:
+ protocol = 'v4'
+
openlog(ident=f'vti-up-down', logoption=LOG_PID, facility=LOG_INFO)
syslog(f'Interface {interface} {verb} {connection}')
- if verb in ['up-client', 'up-host']:
- call('sudo ip route delete default table 220')
-
- vti_link = get_interface_config(interface)
-
- if not vti_link:
- syslog(f'Interface {interface} not found')
- sys.exit(0)
-
- vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ wait_for_commit_lock()
- if verb in ['up-client', 'up-host']:
- if not vti_link_up:
- conf = ConfigTreeQuery()
- _, vti = get_interface_dict(conf.config, ['interfaces', 'vti'], interface)
- if 'disable' not in vti:
- tmp = VTIIf(interface)
- tmp.update(vti)
- call(f'sudo ip link set {interface} up')
- else:
- call(f'sudo ip link set {interface} down')
- syslog(f'Interface {interface} is admin down ...')
- elif verb in ['down-client', 'down-host']:
- if vti_link_up:
- call(f'sudo ip link set {interface} down')
+ if verb in ['up-client', 'up-client-v6', 'up-host', 'up-host-v6']:
+ with open_vti_updown_db_for_update() as db:
+ db.add(interface, connection, protocol)
+ db.commit(supply_interface_dict)
+ elif verb in ['down-client', 'down-client-v6', 'down-host', 'down-host-v6']:
+ with open_vti_updown_db_for_update() as db:
+ db.remove(interface, connection, protocol)
+ db.commit(supply_interface_dict)
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index c9b8ef8fe..76be41ddc 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -110,3 +110,8 @@ net.ipv6.conf.all.seg6_enabled = 0
net.ipv6.conf.default.seg6_enabled = 0
net.vrf.strict_mode = 1
+
+# https://vyos.dev/T6570
+# By default, do not forward traffic from bridge to IPvX layer
+net.bridge.bridge-nf-call-iptables = 0
+net.bridge.bridge-nf-call-ip6tables = 0 \ No newline at end of file
diff --git a/src/migration-scripts/openvpn/1-to-2 b/src/migration-scripts/openvpn/1-to-2
index b7b7d4c77..2baa7302c 100644
--- a/src/migration-scripts/openvpn/1-to-2
+++ b/src/migration-scripts/openvpn/1-to-2
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
# Remove 'encryption cipher' and add this value to 'encryption ncp-ciphers'
# for server and client mode.
# Site-to-site mode still can use --cipher option
diff --git a/src/migration-scripts/openvpn/2-to-3 b/src/migration-scripts/openvpn/2-to-3
index 0b9073ae6..4e6b3c8b7 100644
--- a/src/migration-scripts/openvpn/2-to-3
+++ b/src/migration-scripts/openvpn/2-to-3
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
mode = config.return_value(['interfaces', 'openvpn', i, 'mode'])
if mode != 'server':
# If it's a client or a site-to-site OpenVPN interface,
diff --git a/src/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4
index d3c76c7d3..0529491c1 100644
--- a/src/migration-scripts/openvpn/3-to-4
+++ b/src/migration-scripts/openvpn/3-to-4
@@ -18,11 +18,7 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
for i in ovpn_intfs:
#Rename 'encryption ncp-ciphers' with 'encryption data-ciphers'
ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers']
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
index 2d96fe217..1d2f1067a 100755
--- a/src/op_mode/generate_ovpn_client_file.py
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -19,42 +19,53 @@ import argparse
from jinja2 import Template
from textwrap import fill
-from vyos.configquery import ConfigTreeQuery
+from vyos.config import Config
from vyos.ifconfig import Section
client_config = """
client
nobind
-remote {{ remote_host }} {{ port }}
+remote {{ local_host if local_host else 'x.x.x.x' }} {{ port }}
remote-cert-tls server
-proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }}
-dev {{ device }}
-dev-type {{ device }}
+proto {{ 'tcp-client' if protocol == 'tcp-passive' else 'udp' }}
+dev {{ device_type }}
+dev-type {{ device_type }}
persist-key
persist-tun
verb 3
# Encryption options
+{# Define the encryption map #}
+{% set encryption_map = {
+ 'des': 'DES-CBC',
+ '3des': 'DES-EDE3-CBC',
+ 'bf128': 'BF-CBC',
+ 'bf256': 'BF-CBC',
+ 'aes128gcm': 'AES-128-GCM',
+ 'aes128': 'AES-128-CBC',
+ 'aes192gcm': 'AES-192-GCM',
+ 'aes192': 'AES-192-CBC',
+ 'aes256gcm': 'AES-256-GCM',
+ 'aes256': 'AES-256-CBC'
+} %}
+
{% if encryption is defined and encryption is not none %}
-{% if encryption.cipher is defined and encryption.cipher is not none %}
-cipher {{ encryption.cipher }}
-{% if encryption.cipher == 'bf128' %}
-keysize 128
-{% elif encryption.cipher == 'bf256' %}
-keysize 256
+{% if encryption.data_ciphers is defined and encryption.data_ciphers is not none %}
+cipher {% for algo in encryption.data_ciphers %}
+{{ encryption_map[algo] if algo in encryption_map.keys() else algo }}{% if not loop.last %}:{% endif %}
+{% endfor %}
+
+data-ciphers {% for algo in encryption.data_ciphers %}
+{{ encryption_map[algo] if algo in encryption_map.keys() else algo }}{% if not loop.last %}:{% endif %}
+{% endfor %}
{% endif %}
-{% endif %}
-{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
-data-ciphers {{ encryption.ncp_ciphers }}
-{% endif %}
{% endif %}
{% if hash is defined and hash is not none %}
auth {{ hash }}
{% endif %}
-keysize 256
-comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
+{{ 'comp-lzo' if use_lzo_compression is defined else '' }}
<ca>
-----BEGIN CERTIFICATE-----
@@ -79,7 +90,7 @@ comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
"""
-config = ConfigTreeQuery()
+config = Config()
base = ['interfaces', 'openvpn']
if not config.exists(base):
@@ -89,10 +100,22 @@ if not config.exists(base):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
- parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True)
- parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True)
- parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True)
- parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store")
+ parser.add_argument(
+ "-i",
+ "--interface",
+ type=str,
+ help='OpenVPN interface the client is connecting to',
+ required=True,
+ )
+ parser.add_argument(
+ "-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True
+ )
+ parser.add_argument(
+ "-c", "--cert", type=str, help='OpenVPN client cerificate', required=True
+ )
+ parser.add_argument(
+ "-k", "--key", type=str, help='OpenVPN client cerificate key', action="store"
+ )
args = parser.parse_args()
interface = args.interface
@@ -114,33 +137,25 @@ if __name__ == '__main__':
if not config.exists(['pki', 'certificate', cert, 'private', 'key']):
exit(f'OpenVPN certificate key "{key}" does not exist!')
- ca = config.value(['pki', 'ca', ca, 'certificate'])
+ config = config.get_config_dict(
+ base + [interface],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True,
+ with_pki=True,
+ )
+
+ ca = config['pki']['ca'][ca]['certificate']
ca = fill(ca, width=64)
- cert = config.value(['pki', 'certificate', cert, 'certificate'])
+ cert = config['pki']['certificate'][cert]['certificate']
cert = fill(cert, width=64)
- key = config.value(['pki', 'certificate', key, 'private', 'key'])
+ key = config['pki']['certificate'][key]['private']['key']
key = fill(key, width=64)
- remote_host = config.value(base + [interface, 'local-host'])
-
- ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True)
-
- port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port']
- proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol']
- device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type']
-
- config = {
- 'interface' : interface,
- 'ca' : ca,
- 'cert' : cert,
- 'key' : key,
- 'device' : device,
- 'port' : port,
- 'proto' : proto,
- 'remote_host' : remote_host,
- 'address' : [],
- }
-
-# Clear out terminal first
-print('\x1b[2J\x1b[H')
-client = Template(client_config, trim_blocks=True).render(config)
-print(client)
+
+ config['ca'] = ca
+ config['cert'] = cert
+ config['key'] = key
+ config['port'] = '1194' if 'local_port' not in config else config['local_port']
+
+ client = Template(client_config, trim_blocks=True).render(config)
+ print(client)
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index b193d8109..cf2bc6d5c 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -105,10 +105,39 @@ vyos2windows_integrity = {
}
# IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would
-# be: 14, 15, 16, 17, 18, 19, 20, 21, 31
-ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
-# Windows 10 only allows a limited set of DH groups
-windows_supported_dh_groups = ['1', '2', '14', '24']
+# be: 14, 15, 16, 17, 18, 19, 20, 21, 31, 32
+vyos2apple_dh_group = {
+ '14' : '14',
+ '15' : '15',
+ '16' : '16',
+ '17' : '17',
+ '18' : '18',
+ '19' : '19',
+ '20' : '20',
+ '21' : '21',
+ '31' : '31',
+ '32' : '32'
+}
+
+# Newer versions of Windows support groups 19 and 20, albeit under a different naming convention
+vyos2windows_dh_group = {
+ '1' : 'Group1',
+ '2' : 'Group2',
+ '14' : 'Group14',
+ '19' : 'ECP256',
+ '20' : 'ECP384',
+ '24' : 'Group24'
+}
+
+# For PFS, Windows also has its own inconsistent naming scheme for each group
+vyos2windows_pfs_group = {
+ '1' : 'PFS1',
+ '2' : 'PFS2',
+ '14' : 'PFS2048',
+ '19' : 'ECP256',
+ '20' : 'ECP384',
+ '24' : 'PFS24'
+}
parser = argparse.ArgumentParser()
parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
@@ -181,7 +210,7 @@ if args.os == 'ios':
# https://stackoverflow.com/a/9427216
data['ca_certificates'] = [dict(t) for t in {tuple(d.items()) for d in data['ca_certificates']}]
-esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
+esp_group = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group']],
key_mangling=('-', '_'), get_first_key=True)
ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'],
key_mangling=('-', '_'), get_first_key=True)
@@ -192,7 +221,29 @@ ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group']
vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher;
vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity;
-supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups;
+vyos2client_dh_group = vyos2apple_dh_group if args.os == 'ios' else vyos2windows_dh_group
+
+def transform_pfs(pfs, ike_dh_group):
+ pfs_enabled = (pfs != 'disable')
+ if pfs == 'enable':
+ pfs_dh_group = ike_dh_group
+ elif pfs.startswith('dh-group'):
+ pfs_dh_group = pfs.removeprefix('dh-group')
+
+ if args.os == 'ios':
+ if pfs_enabled:
+ if pfs_dh_group not in set(vyos2apple_dh_group):
+ exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+ return pfs_dh_group
+ else:
+ return None
+ else:
+ if pfs_enabled:
+ if pfs_dh_group not in set(vyos2windows_pfs_group):
+ exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+ return vyos2windows_pfs_group[ pfs_dh_group ]
+ else:
+ return 'None'
# Create a dictionary containing client conform IKE settings
ike = {}
@@ -201,24 +252,28 @@ for _, proposal in ike_proposal.items():
if {'dh_group', 'encryption', 'hash'} <= set(proposal):
if (proposal['encryption'] in set(vyos2client_cipher) and
proposal['hash'] in set(vyos2client_integrity) and
- proposal['dh_group'] in set(supported_dh_groups)):
+ proposal['dh_group'] in set(vyos2client_dh_group)):
# We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+ # DH group will need to be transformed later after we calculate PFS group
ike.update( { str(count) : proposal } )
count += 1
-# Create a dictionary containing Apple conform ESP settings
+# Create a dictionary containing client conform ESP settings
esp = {}
count = 1
-for _, proposal in esp_proposals.items():
+for _, proposal in esp_group['proposal'].items():
if {'encryption', 'hash'} <= set(proposal):
if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
# We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+ # Copy PFS setting from the group, if present (we will need to
+ # transform this later once the IKE group is selected)
+ proposal['pfs'] = esp_group.get('pfs', 'enable')
esp.update( { str(count) : proposal } )
count += 1
@@ -230,8 +285,10 @@ try:
tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
tmp += '\nSelect one of the above IKE groups: '
data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
- else:
+ elif len(ike) == 1:
data['ike_encryption'] = ike['1']
+ else:
+ exit(f'None of the configured IKE proposals for "{args.connection}" are supported by the client!')
if len(esp) > 1:
tmp = '\n'
@@ -239,12 +296,18 @@ try:
tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
tmp += '\nSelect one of the above ESP groups: '
data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
- else:
+ elif len(esp) == 1:
data['esp_encryption'] = esp['1']
+ else:
+ exit(f'None of the configured ESP proposals for "{args.connection}" are supported by the client!')
except KeyboardInterrupt:
exit("Interrupted")
+# Transform the DH and PFS groups now that all selections are known
+data['esp_encryption']['pfs'] = transform_pfs(data['esp_encryption']['pfs'], data['ike_encryption']['dh_group'])
+data['ike_encryption']['dh_group'] = vyos2client_dh_group[ data['ike_encryption']['dh_group'] ]
+
print('\n\n==== <snip> ====')
if args.os == 'ios':
print(render_to_string('ipsec/ios_profile.j2', data))
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index c8f5072da..02ba126b4 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -13,7 +13,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import pprint
import re
import sys
import typing
diff --git a/src/op_mode/serial.py b/src/op_mode/serial.py
new file mode 100644
index 000000000..a5864872b
--- /dev/null
+++ b/src/op_mode/serial.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys, typing
+
+import vyos.opmode
+from vyos.utils.serial import restart_login_consoles as _restart_login_consoles
+
+def restart_console(device_name: typing.Optional[str]):
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message and a prompt
+ # to continue, verifying that the user acknowledges possible interruptions.
+ if device_name:
+ _restart_login_consoles(prompt_user=True, quiet=False, devices=[device_name])
+ else:
+ _restart_login_consoles(prompt_user=True, quiet=False)
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/tests/test_configd_inspect.py b/src/tests/test_configd_inspect.py
index 98552c8f3..ccd631893 100644
--- a/src/tests/test_configd_inspect.py
+++ b/src/tests/test_configd_inspect.py
@@ -56,8 +56,7 @@ class TestConfigdInspect(TestCase):
m = import_script(s)
for i in f_list:
f = getattr(m, i, None)
- if not f:
- continue
+ self.assertIsNotNone(f, f"'{s}': missing function '{i}'")
sig = signature(f)
par = sig.parameters
l = len(par)