diff options
69 files changed, 1374 insertions, 367 deletions
diff --git a/.github/workflows/add-rebase-label.yml b/.github/workflows/add-rebase-label.yml index 4cf3545ba..9041303af 100644 --- a/.github/workflows/add-rebase-label.yml +++ b/.github/workflows/add-rebase-label.yml @@ -1,14 +1,14 @@ -name: Add rebase label
-
-on:
- pull_request_target:
- types: [synchronize, opened, reopened, labeled, unlabeled]
-
-permissions:
- pull-requests: write
- contents: read
-
-jobs:
- add-rebase-label:
- uses: vyos/.github/.github/workflows/add-rebase-label.yml@current
- secrets: inherit
+name: Add rebase label + +on: + pull_request_target: + types: [synchronize, opened, reopened, labeled, unlabeled] + +permissions: + pull-requests: write + contents: read + +jobs: + add-rebase-label: + uses: vyos/.github/.github/workflows/add-rebase-label.yml@current + secrets: inherit diff --git a/.github/workflows/chceck-pr-message.yml b/.github/workflows/check-pr-message.yml index 625ba2d75..625ba2d75 100644 --- a/.github/workflows/chceck-pr-message.yml +++ b/.github/workflows/check-pr-message.yml diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 9361f4e7c..239842550 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -63,5 +63,9 @@ }, "system_wireless": { "wireless": ["interfaces_wireless"] + }, + "system_option": { + "ip": ["system_ip"], + "ipv6": ["system_ipv6"] } } diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index d87b90473..9729b295e 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index db4db66a7..099bc59da 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2 index 44f35998b..52ef3cb0e 100644 --- a/data/templates/accel-ppp/pptp.config.j2 +++ b/data/templates/accel-ppp/pptp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index 38da829f3..45d0658af 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -16,6 +16,9 @@ net-snmp {% if limits is vyos_defined %} connlimit {% endif %} +{% if extended_scripts is vyos_defined %} +pppd_compat +{% endif %} [core] thread-count={{ thread_count }} diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index 58f0199fa..3a9af2c94 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -63,6 +63,11 @@ life_packets = {{ vti_esp.life_packets }} {% endif %} life_time = {{ vti_esp.lifetime }}s +{% if vti_esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} local_ts = 0.0.0.0/0,::/0 remote_ts = 0.0.0.0/0,::/0 updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}" @@ -108,6 +113,11 @@ life_packets = {{ tunnel_esp.life_packets }} {% endif %} life_time = {{ tunnel_esp.lifetime }}s +{% if tunnel_esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} {% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %} {% if tunnel_conf.local.prefix is vyos_defined %} {% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %} diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2 index 6bced88c7..e384ae972 100644 --- a/data/templates/ipsec/swanctl/remote_access.j2 +++ b/data/templates/ipsec/swanctl/remote_access.j2 @@ -8,6 +8,10 @@ proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }} version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }} send_certreq = no +{% if ike.dead_peer_detection is vyos_defined %} + dpd_timeout = {{ ike.dead_peer_detection.timeout }} + dpd_delay = {{ ike.dead_peer_detection.interval }} +{% endif %} rekey_time = {{ ike.lifetime }}s keyingtries = 0 {% if rw_conf.unique is vyos_defined %} @@ -44,8 +48,18 @@ children { ikev2-vpn { esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }} - rekey_time = {{ esp.lifetime }}s - rand_time = 540s +{% if esp.life_bytes is vyos_defined %} + life_bytes = {{ esp.life_bytes }} +{% endif %} +{% if esp.life_packets is vyos_defined %} + life_packets = {{ esp.life_packets }} +{% endif %} + life_time = {{ esp.lifetime }}s +{% if esp.disable_rekey is vyos_defined %} + rekey_bytes = 0 + rekey_packets = 0 + rekey_time = 0s +{% endif %} dpd_action = clear inactivity = {{ rw_conf.timeout }} {% if rw_conf.replay_window is vyos_defined %} diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2 index 6ac525443..f69519697 100644 --- a/data/templates/openvpn/server.conf.j2 +++ b/data/templates/openvpn/server.conf.j2 @@ -206,8 +206,8 @@ tls-server {% if encryption.cipher is vyos_defined %} cipher {{ encryption.cipher | openvpn_cipher }} {% endif %} -{% if encryption.ncp_ciphers is vyos_defined %} -data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }} +{% if encryption.data_ciphers is vyos_defined %} +data-ciphers {{ encryption.data_ciphers | openvpn_data_ciphers }} {% endif %} {% endif %} providers default diff --git a/debian/control b/debian/control index c5497b7dd..d3f5fb464 100644 --- a/debian/control +++ b/debian/control @@ -308,7 +308,7 @@ Depends: kbd, # End "system option keyboard-layout" # For "container" - podman, + podman (>=4.9.5), netavark, aardvark-dns, # iptables is only used for containers now, not the the firewall CLI diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i index 55ffa3a8b..0acb08ec9 100644 --- a/interface-definitions/include/firewall/common-rule-inet.xml.i +++ b/interface-definitions/include/firewall/common-rule-inet.xml.i @@ -7,7 +7,6 @@ #include <include/generic-disable-node.xml.i> #include <include/firewall/dscp.xml.i> #include <include/firewall/fragment.xml.i> -#include <include/firewall/match-ipsec.xml.i> #include <include/firewall/limit.xml.i> #include <include/firewall/log.xml.i> #include <include/firewall/log-options.xml.i> diff --git a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i index 960c960db..e8da1a0e1 100644 --- a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i @@ -9,7 +9,6 @@ #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/protocol.xml.i> #include <include/firewall/nft-queue.xml.i> #include <include/firewall/recent.xml.i> diff --git a/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i b/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i index 958167b89..3f7c5a0a3 100644 --- a/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i @@ -9,7 +9,6 @@ #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/protocol.xml.i> #include <include/firewall/nft-queue.xml.i> #include <include/firewall/recent.xml.i> diff --git a/interface-definitions/include/firewall/ipv4-hook-input.xml.i b/interface-definitions/include/firewall/ipv4-hook-input.xml.i index cefb1ffa7..491d1a9f3 100644 --- a/interface-definitions/include/firewall/ipv4-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-input.xml.i @@ -27,7 +27,7 @@ <children> #include <include/firewall/common-rule-ipv4.xml.i> #include <include/firewall/inbound-interface.xml.i> - #include <include/firewall/match-ipsec.xml.i> + #include <include/firewall/match-ipsec-in.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/firewall/ipv4-hook-output.xml.i b/interface-definitions/include/firewall/ipv4-hook-output.xml.i index ca47ae09b..ee9157592 100644 --- a/interface-definitions/include/firewall/ipv4-hook-output.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-output.xml.i @@ -26,6 +26,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv4.xml.i> + #include <include/firewall/match-ipsec-out.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> </tagNode> @@ -53,6 +54,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv4-raw.xml.i> + #include <include/firewall/match-ipsec-out.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> </tagNode> diff --git a/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i b/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i index 17ecfe824..b431303ae 100644 --- a/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i @@ -33,6 +33,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv4-raw.xml.i> + #include <include/firewall/match-ipsec-in.xml.i> #include <include/firewall/inbound-interface.xml.i> <leafNode name="jump-target"> <properties> diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i index e1f41e64c..154b10259 100644 --- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i @@ -27,7 +27,7 @@ <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> - #include <include/firewall/match-ipsec.xml.i> + #include <include/firewall/match-ipsec-in.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/firewall/ipv6-hook-output.xml.i b/interface-definitions/include/firewall/ipv6-hook-output.xml.i index f877cfaaf..d3c4c1ead 100644 --- a/interface-definitions/include/firewall/ipv6-hook-output.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-output.xml.i @@ -26,6 +26,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv6.xml.i> + #include <include/firewall/match-ipsec-out.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> </tagNode> @@ -53,6 +54,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv6-raw.xml.i> + #include <include/firewall/match-ipsec-out.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> </tagNode> diff --git a/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i b/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i index 3f384828d..21f8de6f9 100644 --- a/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i @@ -33,6 +33,7 @@ </properties> <children> #include <include/firewall/common-rule-ipv6-raw.xml.i> + #include <include/firewall/match-ipsec-in.xml.i> #include <include/firewall/inbound-interface.xml.i> <leafNode name="jump-target"> <properties> diff --git a/interface-definitions/include/firewall/match-ipsec-in.xml.i b/interface-definitions/include/firewall/match-ipsec-in.xml.i new file mode 100644 index 000000000..62ed6466b --- /dev/null +++ b/interface-definitions/include/firewall/match-ipsec-in.xml.i @@ -0,0 +1,21 @@ +<!-- include start from firewall/match-ipsec-in.xml.i --> +<node name="ipsec"> + <properties> + <help>Inbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec-in"> + <properties> + <help>Inbound traffic that was IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none-in"> + <properties> + <help>Inbound traffic that was not IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/match-ipsec-out.xml.i b/interface-definitions/include/firewall/match-ipsec-out.xml.i new file mode 100644 index 000000000..880fdd4d8 --- /dev/null +++ b/interface-definitions/include/firewall/match-ipsec-out.xml.i @@ -0,0 +1,21 @@ +<!-- include start from firewall/match-ipsec-out.xml.i --> +<node name="ipsec"> + <properties> + <help>Outbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec-out"> + <properties> + <help>Outbound traffic to be IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none-out"> + <properties> + <help>Outbound traffic that will not be IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/match-ipsec.xml.i b/interface-definitions/include/firewall/match-ipsec.xml.i index 82c2b324d..d8d31ef1a 100644 --- a/interface-definitions/include/firewall/match-ipsec.xml.i +++ b/interface-definitions/include/firewall/match-ipsec.xml.i @@ -1,21 +1,33 @@ <!-- include start from firewall/match-ipsec.xml.i --> <node name="ipsec"> <properties> - <help>Inbound IPsec packets</help> + <help>IPsec encapsulated packets</help> </properties> <children> - <leafNode name="match-ipsec"> + <leafNode name="match-ipsec-in"> <properties> - <help>Inbound IPsec packets</help> + <help>Inbound traffic that was IPsec encapsulated</help> <valueless/> </properties> </leafNode> - <leafNode name="match-none"> + <leafNode name="match-none-in"> <properties> - <help>Inbound non-IPsec packets</help> + <help>Inbound traffic that was not IPsec encapsulated</help> <valueless/> </properties> </leafNode> + <leafNode name="match-ipsec-out"> + <properties> + <help>Outbound traffic to be IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none-out"> + <properties> + <help>Outbound traffic that will not be IPsec encapsulated</help> + <valueless/> + </properties> + </leafNode> </children> </node> <!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i index 560ed9e5f..a15cf0eec 100644 --- a/interface-definitions/include/version/firewall-version.xml.i +++ b/interface-definitions/include/version/firewall-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/firewall-version.xml.i --> -<syntaxVersion component='firewall' version='16'></syntaxVersion> +<syntaxVersion component='firewall' version='17'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/openvpn-version.xml.i b/interface-definitions/include/version/openvpn-version.xml.i index e03ad55c0..67ef21983 100644 --- a/interface-definitions/include/version/openvpn-version.xml.i +++ b/interface-definitions/include/version/openvpn-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/openvpn-version.xml.i --> -<syntaxVersion component='openvpn' version='3'></syntaxVersion> +<syntaxVersion component='openvpn' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 1860523c2..13ef3ae5b 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -87,7 +87,7 @@ </constraint> </properties> </leafNode> - <leafNode name="ncp-ciphers"> + <leafNode name="data-ciphers"> <properties> <help>Cipher negotiation list for use in server or client mode</help> <completionHelp> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 7f425d982..4a7fde75b 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -99,6 +99,12 @@ </constraint> </properties> </leafNode> + <leafNode name="disable-rekey"> + <properties> + <help>Do not locally initiate a re-key of the SA, remote peer must re-key before expiration</help> + <valueless/> + </properties> + </leafNode> <leafNode name="mode"> <properties> <help>ESP mode</help> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 664df28cc..40399f481 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -366,10 +366,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output.append(f'ip{def_suffix} dscp != {{{negated_dscp_str}}}') if 'ipsec' in rule_conf: - if 'match_ipsec' in rule_conf['ipsec']: + if 'match_ipsec_in' in rule_conf['ipsec']: output.append('meta ipsec == 1') - if 'match_none' in rule_conf['ipsec']: + if 'match_none_in' in rule_conf['ipsec']: output.append('meta ipsec == 0') + if 'match_ipsec_out' in rule_conf['ipsec']: + output.append('rt ipsec exists') + if 'match_none_out' in rule_conf['ipsec']: + output.append('rt ipsec missing') if 'fragment' in rule_conf: # Checking for fragmentation after priority -400 is not possible, diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 117479ade..72d3d3afe 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -37,6 +37,7 @@ from vyos.utils.network import mac2eui64 from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_namespace +from vyos.utils.network import get_vrf_tableid from vyos.utils.network import is_netns_interface from vyos.utils.process import is_systemd_service_active from vyos.utils.process import run @@ -382,6 +383,9 @@ class Interface(Control): # can not delete ALL interfaces, see below self.flush_addrs() + # remove interface from conntrack VRF interface map + self._del_interface_from_ct_iface_map() + # --------------------------------------------------------------------- # Any class can define an eternal regex in its definition # interface matching the regex will not be deleted @@ -402,29 +406,20 @@ class Interface(Control): if netns: cmd = f'ip netns exec {netns} {cmd}' return self._cmd(cmd) - def _set_vrf_ct_zone(self, vrf): - """ - Add/Remove rules in nftables to associate traffic in VRF to an - individual conntack zone - """ - # Don't allow for netns yet - if 'netns' in self.config: - return None + def _nft_check_and_run(self, nft_command): + # Check if deleting is possible first to avoid raising errors + _, err = self._popen(f'nft --check {nft_command}') + if not err: + # Remove map element + self._cmd(f'nft {nft_command}') - if vrf: - # Get routing table ID for VRF - vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get( - 'info_data', {}).get('table') - # Add map element with interface and zone ID - if vrf_table_id: - self._cmd(f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}') - else: - nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' - # Check if deleting is possible first to avoid raising errors - _, err = self._popen(f'nft --check {nft_del_element}') - if not err: - # Remove map element - self._cmd(f'nft {nft_del_element}') + def _del_interface_from_ct_iface_map(self): + nft_command = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' + self._nft_check_and_run(nft_command) + + def _add_interface_to_ct_iface_map(self, vrf_table_id: int): + nft_command = f'add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}' + self._nft_check_and_run(nft_command) def get_min_mtu(self): """ @@ -597,12 +592,30 @@ class Interface(Control): >>> Interface('eth0').set_vrf() """ + # Don't allow for netns yet + if 'netns' in self.config: + return False + tmp = self.get_interface('vrf') if tmp == vrf: return False + # Get current VRF table ID + old_vrf_tableid = get_vrf_tableid(self.ifname) self.set_interface('vrf', vrf) - self._set_vrf_ct_zone(vrf) + + if vrf: + # Get routing table ID number for VRF + vrf_table_id = get_vrf_tableid(vrf) + # Add map element with interface and zone ID + if vrf_table_id: + # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF + if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id): + self._del_interface_from_ct_iface_map() + self._add_interface_to_ct_iface_map(vrf_table_id) + else: + self._del_interface_from_ct_iface_map() + return True def set_arp_cache_tmo(self, tmo): diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 85a89ef8b..c1f2803ee 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -90,9 +90,17 @@ class L2TPv3If(Interface): """ if self.exists(self.ifname): - # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') + # remove all assigned IP addresses from interface - this is a bit redundant + # as the kernel will remove all addresses on interface deletion + self.flush_addrs() + + # remove interface from conntrack VRF interface map, here explicitly and do not + # rely on the base class implementation as the interface will + # vanish as soon as the l2tp session is deleted + self._del_interface_from_ct_iface_map() + if {'tunnel_id', 'session_id'} <= set(self.config): cmd = 'ip l2tp del session tunnel_id {tunnel_id}' cmd += ' session_id {session_id}' @@ -101,3 +109,5 @@ class L2TPv3If(Interface): if 'tunnel_id' in self.config: cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' self._cmd(cmd.format(**self.config)) + + # No need to call the baseclass as the interface is now already gone diff --git a/python/vyos/ipsec.py b/python/vyos/ipsec.py index 4603aab22..28f77565a 100644 --- a/python/vyos/ipsec.py +++ b/python/vyos/ipsec.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-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 @@ -13,31 +13,38 @@ # 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/>. -#Package to communicate with Strongswan VICI +# Package to communicate with Strongswan VICI + class ViciInitiateError(Exception): """ - VICI can't initiate a session. + VICI can't initiate a session. """ + pass + + class ViciCommandError(Exception): """ - VICI can't execute a command by any reason. + VICI can't execute a command by any reason. """ + pass + def get_vici_sas(): from vici import Session as vici_session try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec not initialized') try: sas = list(session.list_sas()) return sas except Exception: - raise ViciCommandError(f'Failed to get SAs') + raise ViciCommandError('Failed to get SAs') + def get_vici_connections(): from vici import Session as vici_session @@ -45,18 +52,19 @@ def get_vici_connections(): try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec not initialized') try: connections = list(session.list_conns()) return connections except Exception: - raise ViciCommandError(f'Failed to get connections') + raise ViciCommandError('Failed to get connections') + def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list: """ - Find sas by IKE_SA name and/or CHILD_SA name - and return list of OrdinaryDicts with SASs info - If tunnel is not None return value is list of OrdenaryDicts contained only + Find installed SAs by IKE_SA name and/or CHILD_SA name + and return list with SASs info. + If tunnel is not None return a list contained only CHILD_SAs wich names equal tunnel value. :param ike_name: IKE SA name :type ike_name: str @@ -70,7 +78,7 @@ def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list: try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec not initialized') vici_dict = {} if ike_name: vici_dict['ike'] = ike_name @@ -80,7 +88,31 @@ def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list: sas = list(session.list_sas(vici_dict)) return sas except Exception: - raise ViciCommandError(f'Failed to get SAs') + raise ViciCommandError('Failed to get SAs') + + +def get_vici_connection_by_name(ike_name: str) -> list: + """ + Find loaded SAs by IKE_SA name and return list with SASs info + :param ike_name: IKE SA name + :type ike_name: str + :return: list of Ordinary Dicts with SASs + :rtype: list + """ + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError('IPsec is not initialized') + vici_dict = {} + if ike_name: + vici_dict['ike'] = ike_name + try: + sas = list(session.list_conns(vici_dict)) + return sas + except Exception: + raise ViciCommandError('Failed to get SAs') def terminate_vici_ikeid_list(ike_id_list: list) -> None: @@ -94,19 +126,17 @@ def terminate_vici_ikeid_list(ike_id_list: list) -> None: try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec is not initialized') try: for ikeid in ike_id_list: - session_generator = session.terminate( - {'ike-id': ikeid, 'timeout': '-1'}) + session_generator = session.terminate({'ike-id': ikeid, 'timeout': '-1'}) # a dummy `for` loop is required because of requirements # from vici. Without a full iteration on the output, the # command to vici may not be executed completely for _ in session_generator: pass except Exception: - raise ViciCommandError( - f'Failed to terminate SA for IKE ids {ike_id_list}') + raise ViciCommandError(f'Failed to terminate SA for IKE ids {ike_id_list}') def terminate_vici_by_name(ike_name: str, child_name: str) -> None: @@ -123,9 +153,9 @@ def terminate_vici_by_name(ike_name: str, child_name: str) -> None: try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec is not initialized') try: - vici_dict: dict= {} + vici_dict: dict = {} if ike_name: vici_dict['ike'] = ike_name if child_name: @@ -138,16 +168,48 @@ def terminate_vici_by_name(ike_name: str, child_name: str) -> None: pass except Exception: if child_name: - raise ViciCommandError( - f'Failed to terminate SA for IPSEC {child_name}') + raise ViciCommandError(f'Failed to terminate SA for IPSEC {child_name}') else: - raise ViciCommandError( - f'Failed to terminate SA for IKE {ike_name}') + raise ViciCommandError(f'Failed to terminate SA for IKE {ike_name}') + + +def vici_initiate_all_child_sa_by_ike(ike_sa_name: str, child_sa_list: list) -> bool: + """ + Initiate IKE SA with scpecified CHILD_SAs in list + + Args: + ike_sa_name (str): an IKE SA connection name + child_sa_list (list): a list of child SA names + + Returns: + bool: a result of initiation command + """ + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise ViciInitiateError('IPsec is not initialized') + + try: + for child_sa_name in child_sa_list: + session_generator = session.initiate( + {'ike': ike_sa_name, 'child': child_sa_name, 'timeout': '-1'} + ) + # a dummy `for` loop is required because of requirements + # from vici. Without a full iteration on the output, the + # command to vici may not be executed completely + for _ in session_generator: + pass + return True + except Exception: + raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}') -def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str, - dst_addr: str) -> bool: - """Initiate IKE SA connection with specific peer +def vici_initiate( + ike_sa_name: str, child_sa_name: str, src_addr: str, dst_addr: str +) -> bool: + """Initiate IKE SA with one child_sa connection with specific peer Args: ike_sa_name (str): an IKE SA connection name @@ -163,16 +225,18 @@ def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str, try: session = vici_session() except Exception: - raise ViciInitiateError("IPsec not initialized") + raise ViciInitiateError('IPsec is not initialized') try: - session_generator = session.initiate({ - 'ike': ike_sa_name, - 'child': child_sa_name, - 'timeout': '-1', - 'my-host': src_addr, - 'other-host': dst_addr - }) + session_generator = session.initiate( + { + 'ike': ike_sa_name, + 'child': child_sa_name, + 'timeout': '-1', + 'my-host': src_addr, + 'other-host': dst_addr, + } + ) # a dummy `for` loop is required because of requirements # from vici. Without a full iteration on the output, the # command to vici may not be executed completely @@ -180,4 +244,4 @@ def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str, pass return True except Exception: - raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}')
\ No newline at end of file + raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index a9819dc4b..a6c64adfb 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -89,7 +89,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release)", name): return True else: return False diff --git a/python/vyos/template.py b/python/vyos/template.py index e8d7ba669..3507e0940 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -556,8 +556,8 @@ def get_openvpn_cipher(cipher): return openvpn_translate[cipher].upper() return cipher.upper() -@register_filter('openvpn_ncp_ciphers') -def get_openvpn_ncp_ciphers(ciphers): +@register_filter('openvpn_data_ciphers') +def get_openvpn_data_ciphers(ciphers): out = [] for cipher in ciphers: if cipher in openvpn_translate: diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 829124b57..8fce08de0 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -83,6 +83,19 @@ def get_interface_vrf(interface): return tmp['master'] return 'default' +def get_vrf_tableid(interface: str): + """ Return VRF table ID for given interface name or None """ + from vyos.utils.dict import dict_search + table = None + tmp = get_interface_config(interface) + # Check if we are "the" VRF interface + if dict_search('linkinfo.info_kind', tmp) == 'vrf': + table = tmp['linkinfo']['info_data']['table'] + # or an interface bound to a VRF + elif dict_search('linkinfo.info_slave_kind', tmp) == 'vrf': + table = tmp['linkinfo']['info_slave_data']['table'] + return table + def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. @@ -537,22 +550,50 @@ def ipv6_prefix_length(low, high): return None xor = bytearray(a ^ b for a, b in zip(lo, hi)) - + plen = 0 while plen < 128 and xor[plen // 8] == 0: plen += 8 - + if plen == 128: return plen - + for i in range((plen // 8) + 1, 16): if xor[i] != 0: return None - + for i in range(8): msk = ~xor[plen // 8] & 0xff - + if msk == bytemasks[i]: return plen + i + 1 return None + +def get_nft_vrf_zone_mapping() -> dict: + """ + Retrieve current nftables conntrack mapping list from Kernel + + returns: [{'interface': 'red', 'vrf_tableid': 1000}, + {'interface': 'eth2', 'vrf_tableid': 1000}, + {'interface': 'blue', 'vrf_tableid': 2000}] + """ + from json import loads + from jmespath import search + from vyos.utils.process import cmd + output = [] + tmp = loads(cmd('sudo nft -j list table inet vrf_zones')) + # {'nftables': [{'metainfo': {'json_schema_version': 1, + # 'release_name': 'Old Doc Yak #3', + # 'version': '1.0.9'}}, + # {'table': {'family': 'inet', 'handle': 6, 'name': 'vrf_zones'}}, + # {'map': {'elem': [['eth0', 666], + # ['dum0', 666], + # ['wg500', 666], + # ['bond10.666', 666]], + vrf_list = search('nftables[].map.elem | [0]', tmp) + if not vrf_list: + return output + for (vrf_name, vrf_id) in vrf_list: + output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id}) + return output diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index 60ef87a51..ce880f4a4 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -225,7 +225,7 @@ def process_named_running(name: str, cmdline: str=None, timeout: int=0): if not tmp: if time.time() > time_expire: break - time.sleep(0.100) # wait 250ms + time.sleep(0.100) # wait 100ms continue return tmp else: diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index 67af456f4..d6b00c678 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -33,7 +33,7 @@ set interfaces ethernet eth1 mtu '9000' set interfaces ethernet eth1 offload gro set interfaces ethernet eth1 speed 'auto' set interfaces loopback lo -set interfaces openvpn vtun0 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun0 encryption data-ciphers 'aes256' set interfaces openvpn vtun0 hash 'sha512' set interfaces openvpn vtun0 ip adjust-mss '1380' set interfaces openvpn vtun0 ip source-validation 'strict' @@ -52,7 +52,7 @@ set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_2' set interfaces openvpn vtun0 tls certificate 'openvpn_vtun0' set interfaces openvpn vtun1 authentication password 'vyos1' set interfaces openvpn vtun1 authentication username 'vyos1' -set interfaces openvpn vtun1 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun1 encryption data-ciphers 'aes256' set interfaces openvpn vtun1 hash 'sha1' set interfaces openvpn vtun1 ip adjust-mss '1380' set interfaces openvpn vtun1 keep-alive failure-count '3' @@ -77,7 +77,7 @@ set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_2' set interfaces openvpn vtun2 authentication password 'vyos2' set interfaces openvpn vtun2 authentication username 'vyos2' set interfaces openvpn vtun2 disable -set interfaces openvpn vtun2 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun2 encryption data-ciphers 'aes256' set interfaces openvpn vtun2 hash 'sha512' set interfaces openvpn vtun2 ip adjust-mss '1380' set interfaces openvpn vtun2 keep-alive failure-count '3' diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 9be2c2f1a..e7e29387f 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -15,7 +15,6 @@ from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses -from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM @@ -25,12 +24,15 @@ from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.file import read_file from vyos.utils.dict import dict_search +from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.network import get_interface_config from vyos.utils.network import get_interface_vrf -from vyos.utils.process import cmd +from vyos.utils.network import get_vrf_tableid +from vyos.utils.network import interface_exists from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local +from vyos.utils.network import get_nft_vrf_zone_mapping from vyos.xml_ref import cli_defined dhclient_base_dir = directories['isc_dhclient_dir'] @@ -116,8 +118,11 @@ class BasicInterfaceTest: self.cli_commit() # Verify that no previously interface remained on the system + ct_map = get_nft_vrf_zone_mapping() for intf in self._interfaces: - self.assertNotIn(intf, interfaces()) + self.assertFalse(interface_exists(intf)) + for map_entry in ct_map: + self.assertNotEqual(intf, map_entry['interface']) # No daemon started during tests should remain running for daemon in ['dhcp6c', 'dhclient']: @@ -257,6 +262,69 @@ class BasicInterfaceTest: self.cli_delete(['vrf', 'name', vrf_name]) + def test_move_interface_between_vrf_instances(self): + if not self._test_vrf: + self.skipTest('not supported') + + vrf1_name = 'smoketest_mgmt1' + vrf1_table = '5424' + vrf2_name = 'smoketest_mgmt2' + vrf2_table = '7412' + + self.cli_set(['vrf', 'name', vrf1_name, 'table', vrf1_table]) + self.cli_set(['vrf', 'name', vrf2_name, 'table', vrf2_table]) + + # move interface into first VRF + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + self.cli_set(self._base_path + [interface, 'vrf', vrf1_name]) + + self.cli_commit() + + # check that interface belongs to proper VRF + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, vrf1_name) + + tmp = get_interface_config(vrf1_name) + self.assertEqual(int(vrf1_table), get_vrf_tableid(interface)) + + # move interface into second VRF + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'vrf', vrf2_name]) + + self.cli_commit() + + # check that interface belongs to proper VRF + for interface in self._interfaces: + tmp = get_interface_vrf(interface) + self.assertEqual(tmp, vrf2_name) + + tmp = get_interface_config(vrf2_name) + self.assertEqual(int(vrf2_table), get_vrf_tableid(interface)) + + self.cli_delete(['vrf', 'name', vrf1_name]) + self.cli_delete(['vrf', 'name', vrf2_name]) + + def test_add_to_invalid_vrf(self): + if not self._test_vrf: + self.skipTest('not supported') + + # move interface into first VRF + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + self.cli_set(self._base_path + [interface, 'vrf', 'invalid']) + + # check validate() - can not use a non-existing VRF + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'vrf', 'invalid']) + self.cli_set(self._base_path + [interface, 'description', 'test_add_to_invalid_vrf']) + def test_span_mirror(self): if not self._mirror_interfaces: self.skipTest('not supported') diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index 4bcc50453..940306ac3 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -15,6 +15,7 @@ import os import unittest import paramiko +import pprint from time import sleep from typing import Type @@ -80,18 +81,32 @@ class VyOSUnitTestSHIM: self._session.discard() def cli_commit(self): + if self.debug: + print('commit') self._session.commit() # during a commit there is a process opening commit_lock, and run() returns 0 while run(f'sudo lsof -nP {commit_lock}') == 0: sleep(0.250) + def op_mode(self, path : list) -> None: + """ + Execute OP-mode command and return stdout + """ + if self.debug: + print('commit') + path = ' '.join(path) + out = cmd(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {path}') + if self.debug: + print(f'\n\ncommand "{path}" returned:\n') + pprint.pprint(out) + return out + def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''): """ Retrieve current "running configuration" from FRR """ command = f'vtysh -c "show run {daemon} no-header"' if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"' out = cmd(command) if self.debug: - import pprint print(f'\n\ncommand "{command}" returned:\n') pprint.pprint(out) return out diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 0943d8e24..e6317050c 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -995,5 +995,81 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') + def test_ipsec_metadata_match(self): + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'ipsec', 'match-ipsec-in']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'ipsec', 'match-none-in']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'action', 'continue']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'ipsec', 'match-ipsec-out']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'ipsec', 'match-none-out']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'ipsec', 'match-ipsec-in']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'ipsec', 'match-none-in']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'action', 'continue']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'ipsec', 'match-ipsec-out']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'ipsec', 'match-none-out']) + + self.cli_commit() + + nftables_search = [ + ['meta ipsec exists', 'accept comment'], + ['meta ipsec missing', 'drop comment'], + ['rt ipsec exists', 'continue comment'], + ['rt ipsec missing', 'reject comment'], + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) + self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) + + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4']) + + # All valid directional usage of ipsec matches + self.cli_commit() + + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) + + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in-indirect']) + + # nft does not support ANY usage of 'meta ipsec' under an output hook, it will fail to load cfg + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_cyclic_jump_validation(self): + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'jump-target', 'smoketest-cycle-2']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'jump-target', 'smoketest-cycle-3']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'log']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-cycle-1']) + + # Multi-level jumps are unwise but allowed + self.cli_commit() + + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump']) + self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1']) + + # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. + # It should be caught by conf validation. + with self.assertRaises(ConfigSessionError): + self.cli_commit() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py index af3d49f75..28165736b 100755 --- a/smoketest/scripts/cli/test_interfaces_l2tpv3.py +++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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,13 +14,12 @@ # 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 import json import unittest from base_interfaces_test import BasicInterfaceTest from vyos.utils.process import cmd - +from vyos.utils.kernel import unload_kmod class L2TPv3InterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -62,7 +61,6 @@ if __name__ == '__main__': # reloaded on demand - not needed but test more and more features for module in ['l2tp_ip6', 'l2tp_ip', 'l2tp_eth', 'l2tp_eth', 'l2tp_netlink', 'l2tp_core']: - if os.path.exists(f'/sys/module/{module}'): - cmd(f'sudo rmmod {module}') + unload_kmod(module) unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 9ca661e87..ca47c3218 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -123,7 +123,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): interface = 'vtun2000' path = base_path + [interface] self.cli_set(path + ['mode', 'client']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm']) # check validate() - cannot specify local-port in client mode self.cli_set(path + ['local-port', '5000']) @@ -197,7 +197,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): auth_hash = 'sha1' self.cli_set(path + ['device-type', 'tun']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes256']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes256']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'client']) self.cli_set(path + ['persistent-tunnel']) @@ -371,7 +371,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): port = str(2000 + ii) self.cli_set(path + ['device-type', 'tun']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'server']) self.cli_set(path + ['local-port', port]) @@ -462,8 +462,8 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['mode', 'site-to-site']) - # check validate() - encryption ncp-ciphers cannot be specified in site-to-site mode - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm']) + # check validate() - cipher negotiation cannot be enabled in site-to-site mode + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['encryption']) diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 58aef0001..7bfe0d221 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -22,9 +22,11 @@ from base_interfaces_test import BasicInterfaceTest from glob import glob from vyos.configsession import ConfigSessionError -from vyos.utils.process import process_named_running -from vyos.utils.kernel import check_kmod from vyos.utils.file import read_file +from vyos.utils.kernel import check_kmod +from vyos.utils.network import interface_exists +from vyos.utils.process import process_named_running +from vyos.utils.process import call from vyos.xml_ref import default_value def get_config_value(interface, key): @@ -33,7 +35,7 @@ def get_config_value(interface, key): return tmp[0] wifi_cc_path = ['system', 'wireless', 'country-code'] - +country = 'se' class WirelessInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -66,7 +68,8 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): cls._test_ipv6 = False cls._test_vlan = False - cls.cli_set(cls, wifi_cc_path + ['se']) + cls.cli_set(cls, wifi_cc_path + [country]) + def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly @@ -84,7 +87,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_config(self): # Only set the hostapd (access-point) options - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'ssid' self.cli_set(self._base_path + [interface, 'ssid', ssid]) @@ -161,7 +164,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_vht_mu_beamformer_config(self): # Multi-User-Beamformer - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'vht_mu-beamformer' antennas = '3' @@ -230,7 +233,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_vht_su_beamformer_config(self): # Single-User-Beamformer - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'vht_su-beamformer' antennas = '3' @@ -299,16 +302,14 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_he_config(self): # Only set the hostapd (access-point) options - HE mode for 802.11ax at 6GHz - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'ssid' channel = '1' sae_pw = 'VyOSVyOSVyOS' - country = 'de' bss_color = '37' channel_set_width = '134' center_channel_freq_1 = '15' - self.cli_set(wifi_cc_path + [country]) self.cli_set(self._base_path + [interface, 'ssid', ssid]) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'channel', channel]) @@ -353,10 +354,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_config_value(interface, 'he_oper_centr_freq_seg0_idx') self.assertEqual(center_channel_freq_1, tmp) - # Country code - tmp = get_config_value(interface, 'country_code') - self.assertEqual(country.upper(), tmp) - # BSS coloring tmp = get_config_value(interface, 'he_bss_color') self.assertEqual(bss_color, tmp) @@ -386,15 +383,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): def test_wireless_hostapd_wpa_config(self): # Only set the hostapd (access-point) options - interface = 'wlan1' - phy = 'phy0' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-SMOKETEST' channel = '1' wpa_key = 'VyOSVyOSVyOS' mode = 'n' - country = 'de' - self.cli_set(self._base_path + [interface, 'physical-device', phy]) self.cli_set(self._base_path + [interface, 'type', 'access-point']) self.cli_set(self._base_path + [interface, 'mode', mode]) @@ -454,7 +448,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.assertTrue(process_named_running('hostapd')) def test_wireless_access_point_bridge(self): - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-Test' bridge = 'br42477' @@ -491,7 +485,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(bridge_path) def test_wireless_security_station_address(self): - interface = 'wlan1' + interface = self._interfaces[1] # wlan1 ssid = 'VyOS-ACL' hostapd_accept_station_conf = f'/run/hostapd/{interface}_station_accept.conf' @@ -511,6 +505,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() + self.assertTrue(interface_exists(interface)) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf')) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf')) + + self.assertTrue(process_named_running('hostapd')) + # in accept mode all addresses are allowed unless specified in the deny list tmp = get_config_value(interface, 'macaddr_acl') self.assertEqual(tmp, '0') @@ -526,6 +526,11 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # Switch mode accept -> deny self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'deny']) self.cli_commit() + + self.assertTrue(interface_exists(interface)) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf')) + self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf')) + # In deny mode all addresses are denied unless specified in the allow list tmp = get_config_value(interface, 'macaddr_acl') self.assertEqual(tmp, '1') @@ -535,4 +540,9 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): if __name__ == '__main__': check_kmod('mac80211_hwsim') - unittest.main(verbosity=2, failfast=True) + # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1) + # remove them to have a clean test start + for interface in ['wlan0', 'wlan1']: + if interface_exists(interface): + call(f'sudo iw dev {interface} del') + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_op-mode_show.py b/smoketest/scripts/cli/test_op-mode_show.py new file mode 100755 index 000000000..fba60cc01 --- /dev/null +++ b/smoketest/scripts/cli/test_op-mode_show.py @@ -0,0 +1,39 @@ +#!/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 + +from vyos.version import get_version + +base_path = ['show'] + +class TestOPModeShow(VyOSUnitTestSHIM.TestCase): + def test_op_mode_show_version(self): + # Retrieve output of "show version" OP-mode command + tmp = self.op_mode(base_path + ['version']) + # Validate + version = get_version() + self.assertIn(f'Version: VyOS {version}', tmp) + + def test_op_mode_show_vrf(self): + # Retrieve output of "show version" OP-mode command + tmp = self.op_mode(base_path + ['vrf']) + # Validate + self.assertIn('VRF is not configured', tmp) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 585c1dc89..905eaf2e9 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -16,7 +16,6 @@ import unittest -from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError @@ -27,6 +26,7 @@ PROCESS_NAME = 'ospfd' base_path = ['protocols', 'ospf'] route_map = 'foo-bar-baz10' +dummy_if = 'dum3562' class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): @classmethod @@ -38,6 +38,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if]) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) @@ -46,6 +47,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['policy', 'route-map', route_map]) + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if]) super(TestProtocolsOSPF, cls).tearDownClass() def tearDown(self): @@ -441,14 +443,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): global_block_high = "399" local_block_low = "400" local_block_high = "499" - interface = 'lo' maximum_stack_size = '5' prefix_one = '192.168.0.1/32' prefix_two = '192.168.0.2/32' prefix_one_value = '1' prefix_two_value = '2' - self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', dummy_if]) self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size]) self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low]) self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high]) @@ -472,17 +473,14 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_15_ldp_sync(self): holddown = "500" - interface = 'lo' interfaces = Section.interfaces('ethernet') - self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['interface', dummy_if]) self.cli_set(base_path + ['ldp-sync', 'holddown', holddown]) # Commit main OSPF changes self.cli_commit() - sleep(10) - # Verify main OSPF changes frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) self.assertIn(f'router ospf', frrconfig) @@ -514,7 +512,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME) self.assertIn(f'interface {interface}', config) self.assertIn(f' ip ospf dead-interval 40', config) - self.assertIn(f' no ip ospf mpls ldp-sync', config) + self.assertNotIn(f' ip ospf mpls ldp-sync', config) def test_ospf_16_graceful_restart(self): period = '300' diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py index c5cf2aab6..f676e2a52 100755 --- a/smoketest/scripts/cli/test_protocols_static.py +++ b/smoketest/scripts/cli/test_protocols_static.py @@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.template import is_ipv6 from vyos.utils.network import get_interface_config +from vyos.utils.network import get_vrf_tableid base_path = ['protocols', 'static'] vrf_path = ['protocols', 'vrf'] @@ -421,7 +422,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): tmp = get_interface_config(vrf) # Compare VRF table ID - self.assertEqual(tmp['linkinfo']['info_data']['table'], int(vrf_config['table'])) + self.assertEqual(get_vrf_tableid(vrf), int(vrf_config['table'])) self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf') # Verify FRR bgpd configuration @@ -478,4 +479,4 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, frrconfig) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index b3daa90d0..7d5eaa440 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -246,5 +246,19 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): for excluded in snmpv3_view_oid_exclude: self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp) + def test_snmp_script_extensions(self): + extensions = { + 'default': 'snmp_smoketest_extension_script.sh', + 'external': '/run/external_snmp_smoketest_extension_script.sh' + } + + for key, val in extensions.items(): + self.cli_set(base_path + ['script-extensions', 'extension-name', key, 'script', val]) + self.cli_commit() + + self.assertEqual(get_config_value('extend default'), f'/config/user-data/{extensions["default"]}') + self.assertEqual(get_config_value('extend external'), extensions["external"]) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py index 030ec587b..45a5b4087 100755 --- a/smoketest/scripts/cli/test_system_syslog.py +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -53,8 +53,8 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) def test_syslog_basic(self): - host1 = '198.51.100.1' - host2 = '192.0.2.1' + host1 = '127.0.0.10' + host2 = '127.0.0.20' self.cli_set(base_path + ['host', host1, 'port', '999']) self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all']) @@ -68,7 +68,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): # *.* @198.51.100.1:999 # kern.err @192.0.2.1:514 config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')] - expected = ['@198.51.100.1:999', '@192.0.2.1:514', '/dev/console'] + expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console'] for i in range(0,3): self.assertIn(expected[i], config[i]) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 27356d70e..2dc66485b 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -252,6 +252,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): for line in swanctl_conf_lines: self.assertIn(line, swanctl_conf) + # if dpd is not specified it should not be enabled (see T6599) + swanctl_unexpected_lines = [ + f'dpd_timeout' + f'dpd_delay' + ] + + for unexpected_line in swanctl_unexpected_lines: + self.assertNotIn(unexpected_line, swanctl_conf) + swanctl_secrets_lines = [ f'id-{regex_uuid4} = "{local_id}"', f'id-{regex_uuid4} = "{remote_id}"', @@ -639,8 +648,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'auth = eap-mschapv2', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'replay_window = 32', f'inactivity = 28800', @@ -761,8 +769,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'auth = eap-tls', f'eap_id = %any', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', @@ -876,8 +883,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'certs = peer1.pem', f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem', f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', - f'rekey_time = {eap_lifetime}s', - f'rand_time = 540s', + f'life_time = {eap_lifetime}s', f'dpd_action = clear', f'inactivity = 28800', f'local_ts = 0.0.0.0/0,::/0', @@ -968,5 +974,117 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_remote_access_no_rekey(self): + # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired + 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' + ike_lifetime = '7200' + eap_lifetime = '3600' + local_id = 'ipsec.vyos.net' + + name_servers = ['172.16.254.100', '172.16.254.101'] + prefix = '172.16.250.0/28' + + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', '0']) + 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, 'disable-rekey']) + 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]) + # Use client-mode x509 instead of default EAP-MSCHAPv2 + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509']) + 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, 'authentication', 'x509', 'ca-certificate', int_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, 'prefix', prefix]) + + 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 = 0s', + f'keyingtries = 0', + f'pools = {ip_pool_name}', + f'id = "{local_id}"', + f'auth = pubkey', + f'certs = peer1.pem', + f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem', + f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256', + f'life_time = {eap_lifetime}s', + f'rekey_time = 0s', + f'dpd_action = clear', + f'inactivity = 28800', + f'local_ts = 0.0.0.0/0,::/0', + ] + for line in swanctl_lines: + self.assertIn(line, swanctl_conf) + + swanctl_pool_lines = [ + f'{ip_pool_name}', + f'addrs = {prefix}', + 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(CA_PATH, f'{int_ca_name}.pem'))) + self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem'))) + + 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 243397dc2..176882ca5 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -25,6 +25,7 @@ from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.file import read_file 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.system import sysctl_read @@ -111,8 +112,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): frrconfig = self.getFRRconfig(f'vrf {vrf}') self.assertIn(f' vni {table}', frrconfig) - tmp = get_interface_config(vrf) - self.assertEqual(int(table), tmp['linkinfo']['info_data']['table']) + self.assertEqual(int(table), get_vrf_tableid(vrf)) # Increment table ID for the next run table = str(int(table) + 1) @@ -266,8 +266,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for address in addresses: self.assertTrue(is_intf_addr_assigned(interface, address)) # Verify VRF table ID - tmp = get_interface_config(vrf) - self.assertEqual(int(table), tmp['linkinfo']['info_data']['table']) + self.assertEqual(int(table), get_vrf_tableid(vrf)) # Verify interface is assigned to VRF tmp = get_interface_config(interface) diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py index 4666e98e7..700e4cec7 100755 --- a/smoketest/scripts/system/test_kernel_options.py +++ b/smoketest/scripts/system/test_kernel_options.py @@ -19,8 +19,9 @@ import os import platform import unittest -kernel = platform.release() +from vyos.utils.kernel import check_kmod +kernel = platform.release() class TestKernelModules(unittest.TestCase): """ VyOS makes use of a lot of Kernel drivers, modules and features. The required modules which are essential for VyOS should be tested that they are @@ -35,9 +36,8 @@ class TestKernelModules(unittest.TestCase): super(TestKernelModules, cls).setUpClass() CONFIG = '/proc/config.gz' - if not os.path.isfile(CONFIG): - call('sudo modprobe configs') + check_kmod('configs') with gzip.open(CONFIG, 'rt') as f: cls._config_data = f.read() diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index ec6b86ef2..352d5cbb1 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -128,7 +128,49 @@ def get_config(config=None): return firewall -def verify_rule(firewall, rule_conf, ipv6): +def verify_jump_target(firewall, root_chain, jump_target, ipv6, 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') + + no_ipsec_in = root_chain 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') + # 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}') + targets_pending.append(child_target) + if len(targets_seen) == 7: + path_txt = ' -> '.join(targets_seen) + Warning(f'Deep nesting of jump targets has reached 8 levels deep, following the path {path_txt} -> {child_target}!') + + targets_seen.append(target) + +def verify_rule(firewall, chain_name, rule_conf, ipv6): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') @@ -139,12 +181,10 @@ def verify_rule(firewall, 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 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') + 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) 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') + verify_jump_target(firewall, chain_name, target, ipv6, recursive=False) if rule_conf['action'] == 'offload': if 'offload_target' not in rule_conf: @@ -185,8 +225,10 @@ def verify_rule(firewall, rule_conf, ipv6): raise ConfigError('Limit rate integer cannot be less than 1') if 'ipsec' in rule_conf: - if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']): - raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') + if {'match_ipsec_in', 'match_none_in'} <= set(rule_conf['ipsec']): + raise ConfigError('Cannot specify both "match-ipsec" and "match-none"') + if {'match_ipsec_out', 'match_none_out'} <= set(rule_conf['ipsec']): + raise ConfigError('Cannot specify both "match-ipsec" and "match-none"') if 'recent' in rule_conf: if not {'count', 'time'} <= set(rule_conf['recent']): @@ -349,13 +391,11 @@ def verify(firewall): 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.') - ## Now need to check that default-jump-target exists (other firewall chain/name) - 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') + 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, rule_conf, False) + verify_rule(firewall, name, rule_conf, False) if 'ipv6' in firewall: for name in ['name','forward','input','output', 'prerouting']: @@ -369,13 +409,11 @@ def verify(firewall): 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.') - ## Now need to check that default-jump-target exists (other firewall chain/name) - if target not in dict_search_args(firewall['ipv6'], 'name'): - raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + 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, rule_conf, True) + verify_rule(firewall, name, rule_conf, True) #### ZONESSSS local_zone = False diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py index 769139e0f..007708d4a 100755 --- a/src/conf_mode/interfaces_geneve.py +++ b/src/conf_mode/interfaces_geneve.py @@ -24,6 +24,7 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_bond_bridge_member +from vyos.configverify import verify_vrf from vyos.ifconfig import GeneveIf from vyos.utils.network import interface_exists from vyos import ConfigError @@ -59,6 +60,7 @@ def verify(geneve): verify_mtu_ipv6(geneve) verify_address(geneve) + verify_vrf(geneve) verify_bond_bridge_member(geneve) verify_mirror_redirect(geneve) diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py index e25793543..b9f827bee 100755 --- a/src/conf_mode/interfaces_l2tpv3.py +++ b/src/conf_mode/interfaces_l2tpv3.py @@ -24,6 +24,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_bond_bridge_member +from vyos.configverify import verify_vrf from vyos.ifconfig import L2TPv3If from vyos.utils.kernel import check_kmod from vyos.utils.network import is_addr_assigned @@ -76,6 +77,7 @@ def verify(l2tpv3): verify_mtu_ipv6(l2tpv3) verify_address(l2tpv3) + verify_vrf(l2tpv3) verify_bond_bridge_member(l2tpv3) verify_mirror_redirect(l2tpv3) return None diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 0dc76b39a..a03bd5959 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -235,10 +235,6 @@ def verify_pki(openvpn): def verify(openvpn): if 'deleted' in openvpn: - # remove totp secrets file if totp is not configured - if os.path.isfile(otp_file.format(**openvpn)): - os.remove(otp_file.format(**openvpn)) - verify_bridge_delete(openvpn) return None @@ -326,8 +322,8 @@ def verify(openvpn): if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]: raise ConfigError('Must specify IPv4 "subnet-mask" for local-address') - if dict_search('encryption.ncp_ciphers', openvpn): - raise ConfigError('NCP ciphers can only be used in client or server mode') + if dict_search('encryption.data_ciphers', openvpn): + raise ConfigError('Cipher negotiation can only be used in client or server mode') else: # checks for client-server or site-to-site bridged @@ -524,7 +520,7 @@ def verify(openvpn): if dict_search('encryption.cipher', openvpn): raise ConfigError('"encryption cipher" option is deprecated for TLS mode. ' - 'Use "encryption ncp-ciphers" instead') + 'Use "encryption data-ciphers" instead') if dict_search('encryption.cipher', openvpn) == 'none': print('Warning: "encryption none" was specified!') @@ -635,9 +631,19 @@ def generate_pki_files(openvpn): def generate(openvpn): + if 'deleted' in openvpn: + # remove totp secrets file if totp is not configured + if os.path.isfile(otp_file.format(**openvpn)): + os.remove(otp_file.format(**openvpn)) + return None + + if 'disable' in openvpn: + return None + interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) openvpn['plugin_dir'] = '/usr/lib/openvpn' + # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn @@ -654,9 +660,6 @@ def generate(openvpn): if os.path.isdir(service_dir): rmtree(service_dir, ignore_errors=True) - if 'deleted' in openvpn or 'disable' in openvpn: - return None - # create client config directory on demand makedir(ccd_dir, user, group) diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py index e6a833df7..20629c6c1 100755 --- a/src/conf_mode/interfaces_vti.py +++ b/src/conf_mode/interfaces_vti.py @@ -19,6 +19,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_vrf from vyos.ifconfig import VTIIf from vyos import ConfigError from vyos import airbag @@ -38,6 +39,7 @@ def get_config(config=None): return vti def verify(vti): + verify_vrf(vti) verify_mirror_redirect(vti) return None diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py index aca0a20e4..68646e8ff 100755 --- a/src/conf_mode/interfaces_vxlan.py +++ b/src/conf_mode/interfaces_vxlan.py @@ -28,6 +28,7 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_source_interface from vyos.configverify import verify_bond_bridge_member +from vyos.configverify import verify_vrf from vyos.ifconfig import Interface from vyos.ifconfig import VXLANIf from vyos.template import is_ipv6 @@ -216,6 +217,7 @@ def verify(vxlan): verify_mtu_ipv6(vxlan) verify_address(vxlan) + verify_vrf(vxlan) verify_bond_bridge_member(vxlan) verify_mirror_redirect(vxlan) diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index 5fd7ab6e9..f35a250cb 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -19,6 +19,7 @@ import os from sys import exit from re import findall from netaddr import EUI, mac_unix_expanded +from time import sleep from vyos.config import Config from vyos.configdict import get_interface_dict @@ -34,6 +35,9 @@ from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.kernel import check_kmod from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -93,6 +97,11 @@ def get_config(config=None): if wifi.from_defaults(['security', 'wpa']): # if not set by user del wifi['security']['wpa'] + # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number + if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []): + wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] + del wifi['capabilities']['ht']['40mhz_incapable'] + if dict_search('security.wpa', wifi) != None: wpa_cipher = wifi['security']['wpa'].get('cipher') wpa_mode = wifi['security']['wpa'].get('mode') @@ -120,7 +129,7 @@ def get_config(config=None): tmp = find_other_stations(conf, base, wifi['ifname']) if tmp: wifi['station_interfaces'] = tmp - # used in hostapt.conf.j2 + # used in hostapd.conf.j2 wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi) wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi) @@ -232,11 +241,6 @@ def verify(wifi): def generate(wifi): interface = wifi['ifname'] - # always stop hostapd service first before reconfiguring it - call(f'systemctl stop hostapd@{interface}.service') - # always stop wpa_supplicant service first before reconfiguring it - call(f'systemctl stop wpa_supplicant@{interface}.service') - # Delete config files if interface is removed if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): @@ -272,11 +276,6 @@ def generate(wifi): mac.dialect = mac_unix_expanded wifi['mac'] = str(mac) - # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number - if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []): - wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] - del wifi['capabilities']['ht']['40mhz_incapable'] - # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi) @@ -290,23 +289,45 @@ def generate(wifi): def apply(wifi): interface = wifi['ifname'] + # From systemd source code: + # If there's a stop job queued before we enter the DEAD state, we shouldn't act on Restart=, + # in order to not undo what has already been enqueued. */ + # + # It was found that calling restart on hostapd will (4 out of 10 cases) deactivate + # the service instead of restarting it, when it was not yet properly stopped + # systemd[1]: hostapd@wlan1.service: Deactivated successfully. + # Thus kill all WIFI service and start them again after it's ensured nothing lives + call(f'systemctl stop hostapd@{interface}.service') + call(f'systemctl stop wpa_supplicant@{interface}.service') + if 'deleted' in wifi: - WiFiIf(interface).remove() - else: - # Finally create the new interface - w = WiFiIf(**wifi) - w.update(wifi) - - # Enable/Disable interface - interface is always placed in - # administrative down state in WiFiIf class - if 'disable' not in wifi: - # Physical interface is now configured. Proceed by starting hostapd or - # wpa_supplicant daemon. When type is monitor we can just skip this. - if wifi['type'] == 'access-point': - call(f'systemctl start hostapd@{interface}.service') - - elif wifi['type'] == 'station': - call(f'systemctl start wpa_supplicant@{interface}.service') + WiFiIf(**wifi).remove() + return None + + while (is_systemd_service_running(f'hostapd@{interface}.service') or \ + is_systemd_service_active(f'hostapd@{interface}.service')): + sleep(0.250) # wait 250ms + + # Finally create the new interface + w = WiFiIf(**wifi) + w.update(wifi) + + # Enable/Disable interface - interface is always placed in + # administrative down state in WiFiIf class + if 'disable' not in wifi: + # Wait until interface was properly added to the Kernel + ii = 0 + while not (interface_exists(interface) and ii < 20): + sleep(0.250) # wait 250ms + ii += 1 + + # Physical interface is now configured. Proceed by starting hostapd or + # wpa_supplicant daemon. When type is monitor we can just skip this. + if wifi['type'] == 'access-point': + call(f'systemctl start hostapd@{interface}.service') + + elif wifi['type'] == 'station': + call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index 6f025cc23..c9c0ed9a0 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.py @@ -41,6 +41,7 @@ config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' +default_script_dir = r'/config/user-data/' systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' systemd_service = 'snmpd.service' @@ -85,8 +86,20 @@ def get_config(config=None): tmp = {'::1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) + if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']: + for key, val in snmp['script_extensions']['extension_name'].items(): + if 'script' not in val: + continue + script_path = val['script'] + # if script has not absolute path, use pre configured path + if not os.path.isabs(script_path): + script_path = os.path.join(default_script_dir, script_path) + + snmp['script_extensions']['extension_name'][key]['script'] = script_path + return snmp + def verify(snmp): if 'deleted' in snmp: return None diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 571ce55ec..180686924 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -31,6 +31,7 @@ from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running from vyos.utils.network import is_addr_assigned from vyos.utils.network import is_intf_addr_assigned +from vyos.configdep import set_dependents, call_dependents from vyos import ConfigError from vyos import airbag airbag.enable() @@ -55,6 +56,12 @@ def get_config(config=None): get_first_key=True, with_recursive_defaults=True) + if 'performance' in options: + # Update IPv4 and IPv6 options after TuneD reapplies + # sysctl from config files + for protocol in ['ip', 'ipv6']: + set_dependents(protocol, conf) + return options def verify(options): @@ -145,6 +152,8 @@ def apply(options): else: cmd('systemctl stop tuned.service') + call_dependents() + # Keyboard layout - there will be always the default key inside the dict # but we check for key existence anyway if 'keyboard_layout' in options: diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index dc78c755e..cf82b767f 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -24,6 +24,7 @@ from time import sleep 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 leaf_node_changed @@ -86,9 +87,22 @@ def get_config(config=None): ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, - with_recursive_defaults=True, with_pki=True) + # We have to cleanup the default dict, as default values could + # enable features which are not explicitly enabled on the + # CLI. E.g. dead-peer-detection defaults should not be injected + # unless the feature is explicitly opted in to by setting the + # top-level node + default_values = conf.get_config_defaults(**ipsec.kwargs, recursive=True) + + if 'ike_group' in ipsec: + for name, ike in ipsec['ike_group'].items(): + if 'dead_peer_detection' not in ike: + del default_values['ike_group'][name]['dead_peer_detection'] + + ipsec = config_dict_merge(default_values, ipsec) + ipsec['dhcp_interfaces'] = set() ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 8d8c234c0..184725573 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -26,7 +26,7 @@ from vyos.ifconfig import Interface from vyos.template import render from vyos.template import render_to_string from vyos.utils.dict import dict_search -from vyos.utils.network import get_interface_config +from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call @@ -160,8 +160,8 @@ def verify(vrf): # routing table id can't be changed - OS restriction if interface_exists(name): - tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name))) - if tmp and tmp != vrf_config['table']: + tmp = get_vrf_tableid(name) + if tmp and tmp != int(vrf_config['table']): raise ConfigError(f'VRF "{name}" table id modification not possible!') # VRF routing table ID must be unique on the system diff --git a/src/migration-scripts/firewall/16-to-17 b/src/migration-scripts/firewall/16-to-17 new file mode 100755 index 000000000..ad0706f04 --- /dev/null +++ b/src/migration-scripts/firewall/16-to-17 @@ -0,0 +1,60 @@ +# 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/>. + +# +# T4694: Adding rt ipsec exists/missing match to firewall configs. +# This involves a syntax change for IPsec matches, reflecting that different +# nftables expressions are required depending on whether we're matching a +# decrypted packet or a packet that will be encrypted - it's directional. +# The old rules only matched decrypted packets, those matches are now *-in: + # from: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec|match-none + # to: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec-in|match-none-in +# +# The <chainspec> positions this match allowed were: +# name (any custom chains), forward filter, input filter, prerouting raw. +# There are positions where it was possible to set, but it would never commit +# (nftables rejects 'meta ipsec' in output hooks), they are not considered here. +# + +from vyos.configtree import ConfigTree + +firewall_base = ['firewall'] + +def migrate_chain(config: ConfigTree, path: list[str]) -> None: + if not config.exists(path + ['rule']): + return + + for rule_num in config.list_nodes(path + ['rule']): + tmp_path = path + ['rule', rule_num, 'ipsec'] + if config.exists(tmp_path + ['match-ipsec']): + config.delete(tmp_path + ['match-ipsec']) + config.set(tmp_path + ['match-ipsec-in']) + elif config.exists(tmp_path + ['match-none']): + config.delete(tmp_path + ['match-none']) + config.set(tmp_path + ['match-none-in']) + +def migrate(config: ConfigTree) -> None: + if not config.exists(firewall_base): + # Nothing to do + return + + for family in ['ipv4', 'ipv6']: + tmp_path = firewall_base + [family, 'name'] + if config.exists(tmp_path): + for custom_fwname in config.list_nodes(tmp_path): + migrate_chain(config, tmp_path + [custom_fwname]) + + for base_hook in [['forward', 'filter'], ['input', 'filter'], ['prerouting', 'raw']]: + tmp_path = firewall_base + [family] + base_hook + migrate_chain(config, tmp_path) diff --git a/src/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4 new file mode 100644 index 000000000..d3c76c7d3 --- /dev/null +++ b/src/migration-scripts/openvpn/3-to-4 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# 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/>. +# Renames ncp-ciphers option to data-ciphers + +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: + #Rename 'encryption ncp-ciphers' with 'encryption data-ciphers' + ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers'] + if config.exists(ncp_cipher_path): + config.rename(ncp_cipher_path, 'data-ciphers') diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index 379890c54..8903f916a 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -97,7 +97,7 @@ def main(): group = parser.add_mutually_exclusive_group() group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store_true") group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store_true") - group.add_argument("--interface", help="Interface name", action="store", required=True) + parser.add_argument("--interface", help="Interface name", action="store", required=True) args = parser.parse_args() if args.connect or args.disconnect: diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index 14ffdca9f..e7afc4caa 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -445,12 +445,24 @@ def _format_show_counters(data: list): print (output) return output + +def _show_raw(data: list, intf_name: str): + if intf_name is not None and len(data) <= 1: + try: + return data[0] + except IndexError: + raise vyos.opmode.UnconfiguredObject( + f"Interface {intf_name} does not exist") + else: + return data + + def show(raw: bool, intf_name: typing.Optional[str], intf_type: typing.Optional[str], vif: bool, vrrp: bool): data = _get_raw_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_data(data) def show_summary(raw: bool, intf_name: typing.Optional[str], @@ -458,7 +470,7 @@ def show_summary(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_summary_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_summary(data) def show_summary_extended(raw: bool, intf_name: typing.Optional[str], @@ -466,7 +478,7 @@ def show_summary_extended(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_summary_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_summary_extended(data) def show_counters(raw: bool, intf_name: typing.Optional[str], @@ -474,7 +486,7 @@ def show_counters(raw: bool, intf_name: typing.Optional[str], vif: bool, vrrp: bool): data = _get_counter_data(intf_name, intf_type, vif, vrrp) if raw: - return data + return _show_raw(data, intf_name) return _format_show_counters(data) def clear_counters(intf_name: typing.Optional[str], diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 44d41219e..c8f5072da 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2023 VyOS maintainers and contributors +# Copyright (C) 2022-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 @@ -13,6 +13,7 @@ # # 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 @@ -25,6 +26,7 @@ from vyos.utils.convert import convert_data from vyos.utils.convert import seconds_to_human from vyos.utils.process import cmd from vyos.configquery import ConfigTreeQuery +from vyos.base import Warning import vyos.opmode import vyos.ipsec @@ -43,7 +45,7 @@ def _get_raw_data_sas(): get_sas = vyos.ipsec.get_vici_sas() sas = convert_data(get_sas) return sas - except (vyos.ipsec.ViciInitiateError) as err: + except vyos.ipsec.ViciInitiateError as err: raise vyos.opmode.UnconfiguredSubsystem(err) @@ -56,11 +58,10 @@ def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str: :return: formatted string :rtype: str """ - output = ''; + output = '' for sa_val in ra_output_list: for sa in sa_val.values(): - swanctl_output: str = cmd( - f'sudo swanctl -l --ike-id {sa["uniqueid"]}') + swanctl_output: str = cmd(f'sudo swanctl -l --ike-id {sa["uniqueid"]}') output = f'{output}{swanctl_output}\n\n' return output @@ -72,7 +73,9 @@ def _get_formatted_output_sas(sas): # create an item for each child-sa for child_sa in parent_sa.get('child-sas', {}).values(): # prepare a list for output data - sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A' + sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = ( + sa_out_packets + ) = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A' # collect raw data sa_name = child_sa.get('name') @@ -104,10 +107,8 @@ def _get_formatted_output_sas(sas): bytes_out = filesize.size(int(sa_bytes_out)) sa_out_bytes = f'{bytes_in}/{bytes_out}' if sa_packets_in and sa_packets_out: - packets_in = filesize.size(int(sa_packets_in), - system=filesize.si) - packets_out = filesize.size(int(sa_packets_out), - system=filesize.si) + packets_in = filesize.size(int(sa_packets_in), system=filesize.si) + packets_out = filesize.size(int(sa_packets_out), system=filesize.si) packets_str = f'{packets_in}/{packets_out}' sa_out_packets = re.sub(r'B', r'', packets_str) if sa_remote_addr: @@ -119,7 +120,9 @@ def _get_formatted_output_sas(sas): sa_out_proposal = sa_proposal_encr_alg if sa_proposal_encr_keysize: sa_proposal_encr_keysize_str = sa_proposal_encr_keysize - sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}' + sa_out_proposal = ( + f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}' + ) if sa_proposal_integ_alg: sa_proposal_integ_alg_str = sa_proposal_integ_alg sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}' @@ -128,15 +131,28 @@ def _get_formatted_output_sas(sas): sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}' # add a new item to output data - sa_data.append([ - sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes, - sa_out_packets, sa_out_remote_addr, sa_out_remote_id, - sa_out_proposal - ]) + sa_data.append( + [ + sa_out_name, + sa_out_state, + sa_out_uptime, + sa_out_bytes, + sa_out_packets, + sa_out_remote_addr, + sa_out_remote_id, + sa_out_proposal, + ] + ) headers = [ - "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", - "Remote address", "Remote ID", "Proposal" + 'Connection', + 'State', + 'Uptime', + 'Bytes In/Out', + 'Packets In/Out', + 'Remote address', + 'Remote ID', + 'Proposal', ] sa_data = sorted(sa_data, key=_alphanum_key) output = tabulate(sa_data, headers) @@ -145,14 +161,16 @@ def _get_formatted_output_sas(sas): # Connections block + def _get_convert_data_connections(): try: get_connections = vyos.ipsec.get_vici_connections() connections = convert_data(get_connections) return connections - except (vyos.ipsec.ViciInitiateError) as err: + except vyos.ipsec.ViciInitiateError as err: raise vyos.opmode.UnconfiguredSubsystem(err) + def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: """Get parent SA proposals by connection name if connections not in the 'down' state @@ -184,7 +202,7 @@ def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: 'mode': mode, 'key_size': encr_keysize, 'hash': integ_alg, - 'dh': dh_group + 'dh': dh_group, } return proposal return {} @@ -213,8 +231,7 @@ def _get_parent_sa_state(connection_name: str, data: list) -> str: return ike_state -def _get_child_sa_state(connection_name: str, tunnel_name: str, - data: list) -> str: +def _get_child_sa_state(connection_name: str, tunnel_name: str, data: list) -> str: """Get child SA state by connection and tunnel name Args: @@ -236,14 +253,12 @@ def _get_child_sa_state(connection_name: str, tunnel_name: str, # Get all child SA states # there can be multiple SAs per tunnel child_sa_states = [ - v['state'] for k, v in child_sas.items() if - v['name'] == tunnel_name + v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name ] return 'up' if 'INSTALLED' in child_sa_states else child_sa -def _get_child_sa_info(connection_name: str, tunnel_name: str, - data: list) -> dict: +def _get_child_sa_info(connection_name: str, tunnel_name: str, data: list) -> dict: """Get child SA installed info by connection and tunnel name Args: @@ -264,8 +279,9 @@ def _get_child_sa_info(connection_name: str, tunnel_name: str, # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...} # i.e get all data after 'OFFICE-B-tunnel-0-46' child_sa_info = [ - v for k, v in child_sas.items() if 'name' in v and - v['name'] == tunnel_name and v['state'] == 'INSTALLED' + v + for k, v in child_sas.items() + if 'name' in v and v['name'] == tunnel_name and v['state'] == 'INSTALLED' ] return child_sa_info[-1] if child_sa_info else {} @@ -283,7 +299,7 @@ def _get_child_sa_proposal(child_sa_data: dict) -> dict: 'mode': mode, 'key_size': key_size, 'hash': integ_alg, - 'dh': dh_group + 'dh': dh_group, } return proposal return {} @@ -305,10 +321,10 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: for connection, conn_conf in connections.items(): base_list['ike_connection_name'] = connection base_list['ike_connection_state'] = _get_parent_sa_state( - connection, list_sas) + connection, list_sas + ) base_list['ike_remote_address'] = conn_conf['remote_addrs'] - base_list['ike_proposal'] = _get_parent_sa_proposal( - connection, list_sas) + base_list['ike_proposal'] = _get_parent_sa_proposal(connection, list_sas) base_list['local_id'] = conn_conf.get('local-1', '').get('id') base_list['remote_id'] = conn_conf.get('remote-1', '').get('id') base_list['version'] = conn_conf.get('version', 'IKE') @@ -322,22 +338,25 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: close_action = tun_options.get('close_action') sa_info = _get_child_sa_info(connection, tunnel, list_sas) esp_proposal = _get_child_sa_proposal(sa_info) - base_list['children'].append({ - 'name': tunnel, - 'state': state, - 'local_ts': local_ts, - 'remote_ts': remote_ts, - 'dpd_action': dpd_action, - 'close_action': close_action, - 'sa': sa_info, - 'esp_proposal': esp_proposal - }) + base_list['children'].append( + { + 'name': tunnel, + 'state': state, + 'local_ts': local_ts, + 'remote_ts': remote_ts, + 'dpd_action': dpd_action, + 'close_action': close_action, + 'sa': sa_info, + 'esp_proposal': esp_proposal, + } + ) base_dict.append(base_list) return base_dict def _get_raw_connections_summary(list_conn, list_sas): import jmespath + data = _get_raw_data_connections(list_conn, list_sas) match = '[*].children[]' child = jmespath.search(match, data) @@ -347,17 +366,16 @@ def _get_raw_connections_summary(list_conn, list_sas): 'tunnels': child, 'total': len(child), 'down': tunnels_down, - 'up': tunnels_up + 'up': tunnels_up, } return tun_dict def _get_formatted_output_conections(data): from tabulate import tabulate - data_entries = '' + connections = [] for entry in data: - tunnels = [] ike_name = entry['ike_connection_name'] ike_state = entry['ike_connection_state'] conn_type = entry.get('version', 'IKE') @@ -367,15 +385,26 @@ def _get_formatted_output_conections(data): remote_id = entry['remote_id'] proposal = '-' if entry.get('ike_proposal'): - proposal = (f'{entry["ike_proposal"]["cipher"]}_' - f'{entry["ike_proposal"]["mode"]}/' - f'{entry["ike_proposal"]["key_size"]}/' - f'{entry["ike_proposal"]["hash"]}/' - f'{entry["ike_proposal"]["dh"]}') - connections.append([ - ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts, - local_id, remote_id, proposal - ]) + proposal = ( + f'{entry["ike_proposal"]["cipher"]}_' + f'{entry["ike_proposal"]["mode"]}/' + f'{entry["ike_proposal"]["key_size"]}/' + f'{entry["ike_proposal"]["hash"]}/' + f'{entry["ike_proposal"]["dh"]}' + ) + connections.append( + [ + ike_name, + ike_state, + conn_type, + remote_addrs, + local_ts, + remote_ts, + local_id, + remote_id, + proposal, + ] + ) for tun in entry['children']: tun_name = tun.get('name') tun_state = tun.get('state') @@ -384,18 +413,36 @@ def _get_formatted_output_conections(data): remote_ts = '\n'.join(tun.get('remote_ts')) proposal = '-' if tun.get('esp_proposal'): - proposal = (f'{tun["esp_proposal"]["cipher"]}_' - f'{tun["esp_proposal"]["mode"]}/' - f'{tun["esp_proposal"]["key_size"]}/' - f'{tun["esp_proposal"]["hash"]}/' - f'{tun["esp_proposal"]["dh"]}') - connections.append([ - tun_name, tun_state, conn_type, remote_addrs, local_ts, - remote_ts, local_id, remote_id, proposal - ]) + proposal = ( + f'{tun["esp_proposal"]["cipher"]}_' + f'{tun["esp_proposal"]["mode"]}/' + f'{tun["esp_proposal"]["key_size"]}/' + f'{tun["esp_proposal"]["hash"]}/' + f'{tun["esp_proposal"]["dh"]}' + ) + connections.append( + [ + tun_name, + tun_state, + conn_type, + remote_addrs, + local_ts, + remote_ts, + local_id, + remote_id, + proposal, + ] + ) connection_headers = [ - 'Connection', 'State', 'Type', 'Remote address', 'Local TS', - 'Remote TS', 'Local id', 'Remote id', 'Proposal' + 'Connection', + 'State', + 'Type', + 'Remote address', + 'Local TS', + 'Remote TS', + 'Local id', + 'Remote id', + 'Proposal', ] output = tabulate(connections, connection_headers, numalign='left') return output @@ -421,6 +468,31 @@ def _get_childsa_id_list(ike_sas: list) -> list: return list_childsa_id +def _get_con_childsa_name_list( + ike_sas: list, filter_dict: typing.Optional[dict] = None +) -> list: + """ + Generate list of CHILD SA ids based on list of OrderingDict + wich is returned by vici + :param ike_sas: list of IKE SAs connections generated by vici + :type ike_sas: list + :param filter_dict: dict of filter options + :type filter_dict: dict + :return: list of IKE SAs name + :rtype: list + """ + list_childsa_name: list = [] + for ike in ike_sas: + for ike_name, ike_values in ike.items(): + for sa, sa_values in ike_values['children'].items(): + if filter_dict: + if filter_dict.items() <= sa_values.items(): + list_childsa_name.append(sa) + else: + list_childsa_name.append(sa) + return list_childsa_name + + def _get_all_sitetosite_peers_name_list() -> list: """ Return site-to-site peers configuration @@ -429,53 +501,142 @@ def _get_all_sitetosite_peers_name_list() -> list: """ conf: ConfigTreeQuery = ConfigTreeQuery() config_path = ['vpn', 'ipsec', 'site-to-site', 'peer'] - peers_config = conf.get_config_dict(config_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + peers_config = conf.get_config_dict( + config_path, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) peers_list: list = [] for name in peers_config: peers_list.append(name) return peers_list -def reset_peer(peer: str, tunnel: typing.Optional[str] = None): - # Convert tunnel to Strongwan format of CHILD_SA +def _get_tunnel_sw_format(peer: str, tunnel: str) -> str: + """ + Convert tunnel to Strongwan format of CHILD_SA + :param peer: Peer name (IKE_SA) + :type peer: str + :param tunnel: tunnel number (CHILD_SA) + :type tunnel: str + :return: Converted tunnel name (CHILD_SA) + :rtype: str + """ tunnel_sw = None if tunnel: if tunnel.isnumeric(): tunnel_sw = f'{peer}-tunnel-{tunnel}' elif tunnel == 'vti': tunnel_sw = f'{peer}-vti' + return tunnel_sw + + +def _initiate_peer_with_childsas( + peer: str, tunnel: typing.Optional[str] = None +) -> None: + """ + Initiate IPSEC peer SAs by vici. + If tunnel is None it initiates all peers tunnels + :param peer: Peer name (IKE_SA) + :type peer: str + :param tunnel: tunnel number (CHILD_SA) + :type tunnel: str + """ + tunnel_sw = _get_tunnel_sw_format(peer, tunnel) try: - sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw) - if not sa_list: + con_list: list = vyos.ipsec.get_vici_connection_by_name(peer) + if not con_list: raise vyos.opmode.IncorrectValue( - f'Peer\'s {peer} SA(s) not found, aborting') - if tunnel and sa_list: - childsa_id_list: list = _get_childsa_id_list(sa_list) - if not childsa_id_list: - raise vyos.opmode.IncorrectValue( - f'Peer {peer} tunnel {tunnel} SA(s) not found, aborting') - vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw) - print(f'Peer {peer} reset result: success') - except (vyos.ipsec.ViciInitiateError) as err: + f"Peer's {peer} SA(s) not loaded. Initiation was failed" + ) + childsa_name_list: list = _get_con_childsa_name_list(con_list) + + if not tunnel_sw: + vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, childsa_name_list) + print(f'Peer {peer} initiate result: success') + return + + if tunnel_sw in childsa_name_list: + vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, [tunnel_sw]) + print(f'Peer {peer} tunnel {tunnel} initiate result: success') + return + + raise vyos.opmode.IncorrectValue(f'Peer {peer} SA {tunnel} not found, aborting') + + except vyos.ipsec.ViciInitiateError as err: raise vyos.opmode.UnconfiguredSubsystem(err) - except (vyos.ipsec.ViciCommandError) as err: + except vyos.ipsec.ViciCommandError as err: raise vyos.opmode.IncorrectValue(err) -def reset_all_peers(): +def _terminate_peer(peer: str, tunnel: typing.Optional[str] = None) -> None: + """ + Terminate IPSEC peer SAs by vici. + If tunnel is None it terminates all peers tunnels + :param peer: Peer name (IKE_SA) + :type peer: str + :param tunnel: tunnel number (CHILD_SA) + :type tunnel: str + """ + # Convert tunnel to Strongwan format of CHILD_SA + tunnel_sw = _get_tunnel_sw_format(peer, tunnel) + try: + sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw) + if sa_list: + if tunnel: + childsa_id_list: list = _get_childsa_id_list(sa_list) + if childsa_id_list: + vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw) + print(f'Peer {peer} tunnel {tunnel} terminate result: success') + else: + Warning( + f'Peer {peer} tunnel {tunnel} SA is not initiated. Nothing to terminate' + ) + else: + vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw) + print(f'Peer {peer} terminate result: success') + else: + Warning(f"Peer's {peer} SAs are not initiated. Nothing to terminate") + + except vyos.ipsec.ViciInitiateError as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except vyos.ipsec.ViciCommandError as err: + raise vyos.opmode.IncorrectValue(err) + + +def reset_peer(peer: str, tunnel: typing.Optional[str] = None) -> None: + """ + Reset IPSEC peer SAs. + If tunnel is None it resets all peers tunnels + :param peer: Peer name (IKE_SA) + :type peer: str + :param tunnel: tunnel number (CHILD_SA) + :type tunnel: str + """ + _terminate_peer(peer, tunnel) + peer_config = _get_sitetosite_peer_config(peer) + # initiate SAs only if 'connection-type=initiate' + if ( + 'connection_type' in peer_config + and peer_config['connection_type'] == 'initiate' + ): + _initiate_peer_with_childsas(peer, tunnel) + + +def reset_all_peers() -> None: sitetosite_list = _get_all_sitetosite_peers_name_list() if sitetosite_list: for peer_name in sitetosite_list: try: reset_peer(peer_name) - except (vyos.opmode.IncorrectValue) as err: + except vyos.opmode.IncorrectValue as err: print(err) print('Peers reset result: success') else: raise vyos.opmode.UnconfiguredSubsystem( - 'VPN IPSec site-to-site is not configured, aborting') + 'VPN IPSec site-to-site is not configured, aborting' + ) def _get_ra_session_list_by_username(username: typing.Optional[str] = None): @@ -500,7 +661,7 @@ def _get_ra_session_list_by_username(username: typing.Optional[str] = None): def reset_ra(username: typing.Optional[str] = None): - #Reset remote-access ipsec sessions + # Reset remote-access ipsec sessions if username: list_sa_id = _get_ra_session_list_by_username(username) else: @@ -514,32 +675,47 @@ def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str): ike_sa_name = f'dmvpn-{profile}-{tunnel}' try: # Get IKE SAs - sa_list = convert_data( - vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + sa_list = convert_data(vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) if not sa_list: raise vyos.opmode.IncorrectValue( - f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') - sa_nbma_list = list([x for x in sa_list if - ike_sa_name in x and x[ike_sa_name][ - 'remote-host'] == nbma_dst]) + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting' + ) + sa_nbma_list = list( + [ + x + for x in sa_list + if ike_sa_name in x and x[ike_sa_name]['remote-host'] == nbma_dst + ] + ) if not sa_nbma_list: raise vyos.opmode.IncorrectValue( - f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting') + f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting' + ) # terminate IKE SAs - vyos.ipsec.terminate_vici_ikeid_list(list( - [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if - ike_sa_name in x])) + vyos.ipsec.terminate_vici_ikeid_list( + list( + [ + x[ike_sa_name]['uniqueid'] + for x in sa_nbma_list + if ike_sa_name in x + ] + ) + ) # initiate IKE SAs for ike in sa_nbma_list: if ike_sa_name in ike: - vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', - ike[ike_sa_name]['local-host'], - ike[ike_sa_name]['remote-host']) + vyos.ipsec.vici_initiate( + ike_sa_name, + 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host'], + ) print( - f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success') - except (vyos.ipsec.ViciInitiateError) as err: + f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success' + ) + except vyos.ipsec.ViciInitiateError as err: raise vyos.opmode.UnconfiguredSubsystem(err) - except (vyos.ipsec.ViciCommandError) as err: + except vyos.ipsec.ViciCommandError as err: raise vyos.opmode.IncorrectValue(err) @@ -549,24 +725,30 @@ def reset_profile_all(profile: str, tunnel: str): try: # Get IKE SAs sa_list: list = convert_data( - vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None) + ) if not sa_list: raise vyos.opmode.IncorrectValue( - f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting' + ) # terminate IKE SAs vyos.ipsec.terminate_vici_by_name(ike_sa_name, None) # initiate IKE SAs for ike in sa_list: if ike_sa_name in ike: - vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', - ike[ike_sa_name]['local-host'], - ike[ike_sa_name]['remote-host']) + vyos.ipsec.vici_initiate( + ike_sa_name, + 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host'], + ) print( - f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success') + f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success' + ) print(f'Profile {profile} tunnel {tunnel} reset result: success') - except (vyos.ipsec.ViciInitiateError) as err: + except vyos.ipsec.ViciInitiateError as err: raise vyos.opmode.UnconfiguredSubsystem(err) - except (vyos.ipsec.ViciCommandError) as err: + except vyos.ipsec.ViciCommandError as err: raise vyos.opmode.IncorrectValue(err) @@ -734,36 +916,56 @@ def _get_formatted_output_ra_summary(ra_output_list: list): if child_sa_key: child_sa = sa['child-sas'][child_sa_key] sa_ipsec_proposal = _get_formatted_ipsec_proposal(child_sa) - sa_state = "UP" + sa_state = 'UP' sa_uptime = seconds_to_human(sa['established']) else: sa_ipsec_proposal = '' - sa_state = "DOWN" + sa_state = 'DOWN' sa_uptime = '' sa_data.append( - [sa_id, sa_username, sa_protocol, sa_state, sa_uptime, - sa_tunnel_ip, - sa_remotehost, sa_remoteid, sa_ike_proposal, - sa_ipsec_proposal]) - - headers = ["Connection ID", "Username", "Protocol", "State", "Uptime", - "Tunnel IP", "Remote Host", "Remote ID", "IKE Proposal", - "IPSec Proposal"] + [ + sa_id, + sa_username, + sa_protocol, + sa_state, + sa_uptime, + sa_tunnel_ip, + sa_remotehost, + sa_remoteid, + sa_ike_proposal, + sa_ipsec_proposal, + ] + ) + + headers = [ + 'Connection ID', + 'Username', + 'Protocol', + 'State', + 'Uptime', + 'Tunnel IP', + 'Remote Host', + 'Remote ID', + 'IKE Proposal', + 'IPSec Proposal', + ] sa_data = sorted(sa_data, key=_alphanum_key) output = tabulate(sa_data, headers) return output -def show_ra_detail(raw: bool, username: typing.Optional[str] = None, - conn_id: typing.Optional[str] = None): +def show_ra_detail( + raw: bool, + username: typing.Optional[str] = None, + conn_id: typing.Optional[str] = None, +): list_sa: list = _get_ra_sessions() if username: list_sa = _filter_ikesas(list_sa, 'remote-eap-id', username) elif conn_id: list_sa = _filter_ikesas(list_sa, 'uniqueid', conn_id) if not list_sa: - raise vyos.opmode.IncorrectValue( - f'No active connections found, aborting') + raise vyos.opmode.IncorrectValue('No active connections found, aborting') if raw: return list_sa return _get_output_ra_sas_detail(list_sa) @@ -772,8 +974,7 @@ def show_ra_detail(raw: bool, username: typing.Optional[str] = None, def show_ra_summary(raw: bool): list_sa: list = _get_ra_sessions() if not list_sa: - raise vyos.opmode.IncorrectValue( - f'No active connections found, aborting') + raise vyos.opmode.IncorrectValue('No active connections found, aborting') if raw: return list_sa return _get_formatted_output_ra_summary(list_sa) @@ -783,9 +984,12 @@ def show_ra_summary(raw: bool): def _get_raw_psk(): conf: ConfigTreeQuery = ConfigTreeQuery() config_path = ['vpn', 'ipsec', 'authentication', 'psk'] - psk_config = conf.get_config_dict(config_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) + psk_config = conf.get_config_dict( + config_path, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) psk_list = [] for psk, psk_data in psk_config.items(): @@ -796,11 +1000,13 @@ def _get_raw_psk(): def _get_formatted_psk(psk_list): - headers = ["PSK", "Id", "Secret"] + headers = ['PSK', 'Id', 'Secret'] formatted_data = [] for psk_data in psk_list: - formatted_data.append([psk_data["psk"], "\n".join(psk_data["id"]), psk_data["secret"]]) + formatted_data.append( + [psk_data['psk'], '\n'.join(psk_data['id']), psk_data['secret']] + ) return tabulate(formatted_data, headers=headers) @@ -808,16 +1014,36 @@ def _get_formatted_psk(psk_list): def show_psk(raw: bool): config = ConfigTreeQuery() if not config.exists('vpn ipsec authentication psk'): - raise vyos.opmode.UnconfiguredSubsystem('VPN ipsec psk authentication is not configured') + raise vyos.opmode.UnconfiguredSubsystem( + 'VPN ipsec psk authentication is not configured' + ) psk = _get_raw_psk() if raw: return psk return _get_formatted_psk(psk) + # PSK block end +def _get_sitetosite_peer_config(peer: str): + """ + Return site-to-site peers configuration + :return: site-to-site peers configuration + :rtype: list + """ + conf: ConfigTreeQuery = ConfigTreeQuery() + config_path = ['vpn', 'ipsec', 'site-to-site', 'peer', peer] + peers_config = conf.get_config_dict( + config_path, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) + return peers_config + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py index cfa0678a7..62c683ebb 100755 --- a/src/op_mode/openconnect.py +++ b/src/op_mode/openconnect.py @@ -42,8 +42,10 @@ def _get_formatted_sessions(data): ses_list = [] for ses in data: ses_list.append([ - ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], - ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"] + ses.get("Device", '(none)'), ses.get("Username", '(none)'), + ses.get("IPv4", '(none)'), ses.get("Remote IP", '(none)'), + ses.get("_RX", '(none)'), ses.get("_TX", '(none)'), + ses.get("State", '(none)'), ses.get("_Connected at", '(none)') ]) if len(ses_list) > 0: output = tabulate(ses_list, headers) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 9ce166c7d..84b080023 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -844,7 +844,8 @@ def import_openvpn_secret(name, path): key_version = '1' with open(path) as f: - key_lines = f.read().split("\n") + key_lines = f.read().strip().split("\n") + key_lines = list(filter(lambda line: not line.strip().startswith('#'), key_lines)) # Remove commented lines key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully) diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 87f7c0e25..a4b839a7f 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -143,9 +143,8 @@ def run_script(script_name, config, args) -> int: script.generate(c) script.apply(c) except ConfigError as e: - s = f'{script_name}: {repr(e)}' - logger.error(s) - explicit_print(session_out, session_mode, s) + logger.error(e) + explicit_print(session_out, session_mode, str(e)) return R_ERROR_COMMIT except Exception as e: logger.critical(e) |