summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/check-pr-message.yml (renamed from .github/workflows/chceck-pr-message.yml)0
-rw-r--r--data/templates/conntrack/sysctl.conf.j23
-rw-r--r--data/templates/openvpn/server.conf.j24
-rw-r--r--debian/control3
-rw-r--r--interface-definitions/include/conntrack/log-common.xml.i20
-rw-r--r--interface-definitions/include/conntrack/log-protocols.xml.i26
-rw-r--r--interface-definitions/include/firewall/common-rule-inet.xml.i1
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i1
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-input.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-output.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-input.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-output.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i1
-rw-r--r--interface-definitions/include/firewall/match-ipsec-in.xml.i21
-rw-r--r--interface-definitions/include/firewall/match-ipsec-out.xml.i21
-rw-r--r--interface-definitions/include/firewall/match-ipsec.xml.i22
-rw-r--r--interface-definitions/include/policy/route-common.xml.i18
-rw-r--r--interface-definitions/include/version/firewall-version.xml.i2
-rw-r--r--interface-definitions/include/version/openvpn-version.xml.i2
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in2
-rw-r--r--interface-definitions/system_conntrack.xml.in81
-rw-r--r--python/vyos/configtree.py9
-rw-r--r--python/vyos/defaults.py10
-rw-r--r--python/vyos/firewall.py22
-rw-r--r--python/vyos/ipsec.py136
-rw-r--r--python/vyos/template.py4
-rw-r--r--smoketest/config-tests/dialup-router-medium-vpn6
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py15
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py76
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_l2tpv3.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py10
-rwxr-xr-xsmoketest/scripts/cli/test_op-mode_show.py39
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py49
-rwxr-xr-xsmoketest/scripts/cli/test_system_conntrack.py35
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py6
-rwxr-xr-xsrc/conf_mode/firewall.py70
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py6
-rwxr-xr-xsrc/conf_mode/policy_route.py29
-rwxr-xr-xsrc/conf_mode/system_conntrack.py21
-rwxr-xr-xsrc/migration-scripts/firewall/16-to-1760
-rw-r--r--src/migration-scripts/openvpn/1-to-28
-rw-r--r--src/migration-scripts/openvpn/2-to-38
-rw-r--r--src/migration-scripts/openvpn/3-to-426
-rwxr-xr-xsrc/op_mode/ipsec.py490
-rwxr-xr-xsrc/op_mode/pki.py3
-rwxr-xr-xsrc/services/vyos-configd5
-rwxr-xr-xsrc/services/vyos-conntrack-logger458
-rw-r--r--src/systemd/vyos-conntrack-logger.service21
50 files changed, 1570 insertions, 293 deletions
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/templates/conntrack/sysctl.conf.j2 b/data/templates/conntrack/sysctl.conf.j2
index 554512f4d..cd6c34ede 100644
--- a/data/templates/conntrack/sysctl.conf.j2
+++ b/data/templates/conntrack/sysctl.conf.j2
@@ -6,4 +6,5 @@ net.netfilter.nf_conntrack_max = {{ table_size }}
net.ipv4.tcp_max_syn_backlog = {{ tcp.half_open_connections }}
net.netfilter.nf_conntrack_tcp_loose = {{ '1' if tcp.loose is vyos_defined('enable') else '0' }}
net.netfilter.nf_conntrack_tcp_max_retrans = {{ tcp.max_retrans }}
-net.netfilter.nf_conntrack_acct = {{ '1' if flow_accounting is vyos_defined else '0' }} \ No newline at end of file
+net.netfilter.nf_conntrack_acct = {{ '1' if flow_accounting is vyos_defined else '0' }}
+net.netfilter.nf_conntrack_timestamp = {{ '1' if log.timestamp is vyos_defined else '0' }} \ No newline at end of file
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 189a959b0..d3f5fb464 100644
--- a/debian/control
+++ b/debian/control
@@ -70,6 +70,7 @@ Depends:
python3-netifaces,
python3-paramiko,
python3-passlib,
+ python3-pyroute2,
python3-psutil,
python3-pyhumps,
python3-pystache,
@@ -307,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/conntrack/log-common.xml.i b/interface-definitions/include/conntrack/log-common.xml.i
deleted file mode 100644
index 38799f8f4..000000000
--- a/interface-definitions/include/conntrack/log-common.xml.i
+++ /dev/null
@@ -1,20 +0,0 @@
-<!-- include start from conntrack/log-common.xml.i -->
-<leafNode name="destroy">
- <properties>
- <help>Log connection deletion</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="new">
- <properties>
- <help>Log connection creation</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="update">
- <properties>
- <help>Log connection updates</help>
- <valueless/>
- </properties>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/include/conntrack/log-protocols.xml.i b/interface-definitions/include/conntrack/log-protocols.xml.i
new file mode 100644
index 000000000..019250760
--- /dev/null
+++ b/interface-definitions/include/conntrack/log-protocols.xml.i
@@ -0,0 +1,26 @@
+<!-- include start from conntrack/log-protocols.xml.i -->
+<leafNode name="icmp">
+ <properties>
+ <help>Log connection tracking events for ICMP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="other">
+ <properties>
+ <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="tcp">
+ <properties>
+ <help>Log connection tracking events for TCP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="udp">
+ <properties>
+ <help>Log connection tracking events for UDP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<!-- include end -->
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/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i
index 97795601e..203be73e7 100644
--- a/interface-definitions/include/policy/route-common.xml.i
+++ b/interface-definitions/include/policy/route-common.xml.i
@@ -128,6 +128,24 @@
</completionHelp>
</properties>
</leafNode>
+ <leafNode name="vrf">
+ <properties>
+ <help>VRF to forward packet with</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>default</format>
+ <description>Forward into default global VRF</description>
+ </valueHelp>
+ <completionHelp>
+ <list>default</list>
+ <path>vrf name</path>
+ </completionHelp>
+ #include <include/constraint/vrf.xml.i>
+ </properties>
+ </leafNode>
<leafNode name="tcp-mss">
<properties>
<help>TCP Maximum Segment Size</help>
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/system_conntrack.xml.in b/interface-definitions/system_conntrack.xml.in
index 0dfa2ea81..cd59d1308 100644
--- a/interface-definitions/system_conntrack.xml.in
+++ b/interface-definitions/system_conntrack.xml.in
@@ -223,41 +223,78 @@
</node>
<node name="log">
<properties>
- <help>Log connection tracking events per protocol</help>
+ <help>Log connection tracking</help>
</properties>
<children>
- <node name="icmp">
+ <node name="event">
<properties>
- <help>Log connection tracking events for ICMP</help>
+ <help>Event type and protocol</help>
</properties>
<children>
- #include <include/conntrack/log-common.xml.i>
+ <node name="destroy">
+ <properties>
+ <help>Log connection deletion</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
+ <node name="new">
+ <properties>
+ <help>Log connection creation</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
+ <node name="update">
+ <properties>
+ <help>Log connection updates</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
</children>
</node>
- <node name="other">
+ <leafNode name="timestamp">
<properties>
- <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help>
+ <help>Log connection tracking events include flow-based timestamp</help>
+ <valueless/>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
- <node name="tcp">
+ </leafNode>
+ <leafNode name="queue-size">
<properties>
- <help>Log connection tracking events for TCP</help>
+ <help>Internal message queue size</help>
+ <valueHelp>
+ <format>u32:100-999999</format>
+ <description>Queue size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Queue size must be between 100 and 999999</constraintErrorMessage>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
- <node name="udp">
+ </leafNode>
+ <leafNode name="log-level">
<properties>
- <help>Log connection tracking events for UDP</help>
+ <help>Set log-level. Log must be enable.</help>
+ <completionHelp>
+ <list>info debug</list>
+ </completionHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Info log level</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug log level</description>
+ </valueHelp>
+ <constraint>
+ <regex>(info|debug)</regex>
+ </constraint>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
+ </leafNode>
</children>
</node>
<node name="modules">
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 5775070e2..bd77ab899 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
@@ -290,7 +290,7 @@ class ConfigTree(object):
else:
return True
- def list_nodes(self, path):
+ def list_nodes(self, path, path_must_exist=True):
check_path(path)
path_str = " ".join(map(str, path)).encode()
@@ -298,7 +298,10 @@ class ConfigTree(object):
res = json.loads(res_json)
if res is None:
- raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ if path_must_exist:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return []
else:
return res
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9ccd925ce..25ee45391 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -50,3 +50,13 @@ commit_lock = os.path.join(directories['vyos_configdir'], '.lock')
component_version_json = os.path.join(directories['data'], 'component-versions.json')
config_default = os.path.join(directories['data'], 'config.boot.default')
+
+rt_symbolic_names = {
+ # Standard routing tables for Linux & reserved IDs for VyOS
+ 'default': 253, # Confusingly, a final fallthru, not the default.
+ 'main': 254, # The actual global table used by iproute2 unless told otherwise.
+ 'local': 255, # Special kernel loopback table.
+}
+
+rt_global_vrf = rt_symbolic_names['main']
+rt_global_table = rt_symbolic_names['main']
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 664df28cc..facd498ca 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -30,6 +30,9 @@ from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
# Conntrack
def conntrack_required(conf):
@@ -366,10 +369,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,
@@ -469,11 +476,20 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'mark' in rule_conf['set']:
mark = rule_conf['set']['mark']
output.append(f'meta mark set {mark}')
+ if 'vrf' in rule_conf['set']:
+ set_table = True
+ vrf_name = rule_conf['set']['vrf']
+ if vrf_name == 'default':
+ table = rt_global_vrf
+ else:
+ # NOTE: VRF->table ID lookup depends on the VRF iface already existing.
+ table = get_vrf_tableid(vrf_name)
if 'table' in rule_conf['set']:
set_table = True
table = rule_conf['set']['table']
if table == 'main':
- table = '254'
+ table = rt_global_table
+ if set_table:
mark = 0x7FFFFFFF - int(table)
output.append(f'meta mark set {mark}')
if 'tcp_mss' in rule_conf['set']:
diff --git a/python/vyos/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/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/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_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index 5acfe20fd..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
@@ -87,13 +88,25 @@ class VyOSUnitTestSHIM:
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 abc55e6d2..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,7 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
import json
import unittest
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_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_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 462fc24d0..797ab9770 100755
--- a/smoketest/scripts/cli/test_policy_route.py
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -25,6 +25,8 @@ conn_mark = '555'
conn_mark_set = '111'
table_mark_offset = 0x7fffffff
table_id = '101'
+vrf = 'PBRVRF'
+vrf_table_id = '102'
interface = 'eth0'
interface_wc = 'ppp*'
interface_ip = '172.16.10.1/24'
@@ -39,11 +41,14 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_delete(cls, ['protocols', 'static', 'table', table_id])
+ cls.cli_delete(cls, ['vrf', 'name', vrf])
super(TestPolicyRoute, cls).tearDownClass()
@@ -180,6 +185,50 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
self.verify_rules(ip_rule_search)
+ def test_pbr_vrf(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'vrf', vrf])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'vrf', vrf])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(vrf_table_id))
+
+ # IPv4
+
+ nftables_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR_UD_smoketest'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+
+ nftables6_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+ # IP rule fwmark -> table
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(vrf_table_id)), 'lookup ' + vrf]
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
def test_pbr_matching_criteria(self):
self.cli_set(['policy', 'route', 'smoketest', 'default-log'])
self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'udp'])
diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py
index 3ae7b6217..c07fdce77 100755
--- a/smoketest/scripts/cli/test_system_conntrack.py
+++ b/smoketest/scripts/cli/test_system_conntrack.py
@@ -20,7 +20,7 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.firewall import find_nftables_rule
-from vyos.utils.file import read_file
+from vyos.utils.file import read_file, read_json
base_path = ['system', 'conntrack']
@@ -28,6 +28,9 @@ def get_sysctl(parameter):
tmp = parameter.replace(r'.', r'/')
return read_file(f'/proc/sys/{tmp}')
+def get_logger_config():
+ return read_json('/run/vyos-conntrack-logger.conf')
+
class TestSystemConntrack(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -280,5 +283,35 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack')
self.cli_delete(['firewall'])
+
+ def test_conntrack_log(self):
+ expected_config = {
+ 'event': {
+ 'destroy': {},
+ 'new': {},
+ 'update': {},
+ },
+ 'queue_size': '10000'
+ }
+ self.cli_set(base_path + ['log', 'event', 'destroy'])
+ self.cli_set(base_path + ['log', 'event', 'new'])
+ self.cli_set(base_path + ['log', 'event', 'update'])
+ self.cli_set(base_path + ['log', 'queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('0', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+ for event in ['destroy', 'new', 'update']:
+ for proto in ['icmp', 'other', 'tcp', 'udp']:
+ self.cli_set(base_path + ['log', 'event', event, proto])
+ expected_config['event'][event][proto] = {}
+ self.cli_set(base_path + ['log', 'timestamp'])
+ expected_config['timestamp'] = {}
+ self.cli_commit()
+
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('1', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+
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/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_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 320ab7b7b..a03bd5959 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -322,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
@@ -520,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!')
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index c58fe1bce..223175b8a 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -25,6 +25,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search_args
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -83,6 +86,9 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
if not tcp_flags or 'syn' not in tcp_flags:
raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+ if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF')
+
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
if dict_search_args(rule_conf, 'protocol') != 'tcp':
@@ -152,15 +158,26 @@ def apply_table_marks(policy):
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
for rule_id, rule_conf in pol_conf['rule'].items():
+ vrf_table_id = None
set_table = dict_search_args(rule_conf, 'set', 'table')
- if set_table:
+ set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
+ if set_vrf:
+ if set_vrf == 'default':
+ vrf_table_id = rt_global_vrf
+ else:
+ vrf_table_id = get_vrf_tableid(set_vrf)
+ elif set_table:
if set_table == 'main':
- set_table = '254'
- if set_table in tables:
+ vrf_table_id = rt_global_table
+ else:
+ vrf_table_id = set_table
+ if vrf_table_id is not None:
+ vrf_table_id = int(vrf_table_id)
+ if vrf_table_id in tables:
continue
- tables.append(set_table)
- table_mark = mark_offset - int(set_table)
- cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
+ tables.append(vrf_table_id)
+ table_mark = mark_offset - vrf_table_id
+ cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}')
def cleanup_table_marks():
for cmd_str in ['ip', 'ip -6']:
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
index aa290788c..2529445bf 100755
--- a/src/conf_mode/system_conntrack.py
+++ b/src/conf_mode/system_conntrack.py
@@ -13,7 +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 json
import os
from sys import exit
@@ -24,7 +24,8 @@ from vyos.configdep import set_dependents, call_dependents
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
-from vyos.utils.process import cmd
+from vyos.utils.file import write_file
+from vyos.utils.process import cmd, call
from vyos.utils.process import rc_cmd
from vyos.template import render
from vyos import ConfigError
@@ -34,6 +35,7 @@ airbag.enable()
conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
nftables_ct_file = r'/run/nftables-ct.conf'
+vyos_conntrack_logger_config = r'/run/vyos-conntrack-logger.conf'
# Every ALG (Application Layer Gateway) consists of either a Kernel Object
# also called a Kernel Module/Driver or some rules present in iptables
@@ -113,6 +115,7 @@ def get_config(config=None):
return conntrack
+
def verify(conntrack):
for inet in ['ipv4', 'ipv6']:
if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
@@ -181,6 +184,11 @@ def generate(conntrack):
if not os.path.exists(nftables_ct_file):
conntrack['first_install'] = True
+ if 'log' not in conntrack:
+ # Remove old conntrack-logger config and return
+ if os.path.exists(vyos_conntrack_logger_config):
+ os.unlink(vyos_conntrack_logger_config)
+
# Determine if conntrack is needed
conntrack['ipv4_firewall_action'] = 'return'
conntrack['ipv6_firewall_action'] = 'return'
@@ -199,6 +207,11 @@ def generate(conntrack):
render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+
+ if 'log' in conntrack:
+ log_conf_json = json.dumps(conntrack['log'], indent=4)
+ write_file(vyos_conntrack_logger_config, log_conf_json)
+
return None
def apply(conntrack):
@@ -243,8 +256,12 @@ def apply(conntrack):
# See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
cmd(f'sysctl -f {sysctl_file}')
+ if 'log' in conntrack:
+ call(f'systemctl restart vyos-conntrack-logger.service')
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
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/1-to-2 b/src/migration-scripts/openvpn/1-to-2
index b7b7d4c77..2baa7302c 100644
--- a/src/migration-scripts/openvpn/1-to-2
+++ b/src/migration-scripts/openvpn/1-to-2
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
# Remove 'encryption cipher' and add this value to 'encryption ncp-ciphers'
# for server and client mode.
# Site-to-site mode still can use --cipher option
diff --git a/src/migration-scripts/openvpn/2-to-3 b/src/migration-scripts/openvpn/2-to-3
index 0b9073ae6..4e6b3c8b7 100644
--- a/src/migration-scripts/openvpn/2-to-3
+++ b/src/migration-scripts/openvpn/2-to-3
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
mode = config.return_value(['interfaces', 'openvpn', i, 'mode'])
if mode != 'server':
# If it's a client or a site-to-site OpenVPN interface,
diff --git a/src/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4
new file mode 100644
index 000000000..0529491c1
--- /dev/null
+++ b/src/migration-scripts/openvpn/3-to-4
@@ -0,0 +1,26 @@
+#!/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:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
+ #Rename 'encryption ncp-ciphers' with 'encryption data-ciphers'
+ ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers']
+ if config.exists(ncp_cipher_path):
+ config.rename(ncp_cipher_path, 'data-ciphers')
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/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)
diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger
new file mode 100755
index 000000000..9c31b465f
--- /dev/null
+++ b/src/services/vyos-conntrack-logger
@@ -0,0 +1,458 @@
+#!/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 argparse
+import grp
+import logging
+import multiprocessing
+import os
+import queue
+import signal
+import socket
+import threading
+from datetime import timedelta
+from pathlib import Path
+from time import sleep
+from typing import Dict, AnyStr
+
+from pyroute2 import conntrack
+from pyroute2.netlink import nfnetlink
+from pyroute2.netlink.nfnetlink import NFNL_SUBSYS_CTNETLINK
+from pyroute2.netlink.nfnetlink.nfctsocket import nfct_msg, \
+ IPCTNL_MSG_CT_DELETE, IPCTNL_MSG_CT_NEW, IPS_SEEN_REPLY, \
+ IPS_OFFLOAD, IPS_ASSURED
+
+from vyos.utils.file import read_json
+
+
+shutdown_event = multiprocessing.Event()
+
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+logger = logging.getLogger(__name__)
+
+
+class DebugFormatter(logging.Formatter):
+ def format(self, record):
+ self._style._fmt = '[%(asctime)s] %(levelname)s: %(message)s'
+ return super().format(record)
+
+
+def set_log_level(level: str) -> None:
+ if level == 'debug':
+ logger.setLevel(logging.DEBUG)
+ logger.parent.handlers[0].setFormatter(DebugFormatter())
+ else:
+ logger.setLevel(logging.INFO)
+
+
+EVENT_NAME_TO_GROUP = {
+ 'new': nfnetlink.NFNLGRP_CONNTRACK_NEW,
+ 'update': nfnetlink.NFNLGRP_CONNTRACK_UPDATE,
+ 'destroy': nfnetlink.NFNLGRP_CONNTRACK_DESTROY
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_tcp.h#L9
+TCP_CONNTRACK_SYN_SENT = 1
+TCP_CONNTRACK_SYN_RECV = 2
+TCP_CONNTRACK_ESTABLISHED = 3
+TCP_CONNTRACK_FIN_WAIT = 4
+TCP_CONNTRACK_CLOSE_WAIT = 5
+TCP_CONNTRACK_LAST_ACK = 6
+TCP_CONNTRACK_TIME_WAIT = 7
+TCP_CONNTRACK_CLOSE = 8
+TCP_CONNTRACK_LISTEN = 9
+TCP_CONNTRACK_MAX = 10
+TCP_CONNTRACK_IGNORE = 11
+TCP_CONNTRACK_RETRANS = 12
+TCP_CONNTRACK_UNACK = 13
+TCP_CONNTRACK_TIMEOUT_MAX = 14
+
+TCP_CONNTRACK_TO_NAME = {
+ TCP_CONNTRACK_SYN_SENT: "SYN_SENT",
+ TCP_CONNTRACK_SYN_RECV: "SYN_RECV",
+ TCP_CONNTRACK_ESTABLISHED: "ESTABLISHED",
+ TCP_CONNTRACK_FIN_WAIT: "FIN_WAIT",
+ TCP_CONNTRACK_CLOSE_WAIT: "CLOSE_WAIT",
+ TCP_CONNTRACK_LAST_ACK: "LAST_ACK",
+ TCP_CONNTRACK_TIME_WAIT: "TIME_WAIT",
+ TCP_CONNTRACK_CLOSE: "CLOSE",
+ TCP_CONNTRACK_LISTEN: "LISTEN",
+ TCP_CONNTRACK_MAX: "MAX",
+ TCP_CONNTRACK_IGNORE: "IGNORE",
+ TCP_CONNTRACK_RETRANS: "RETRANS",
+ TCP_CONNTRACK_UNACK: "UNACK",
+ TCP_CONNTRACK_TIMEOUT_MAX: "TIMEOUT_MAX",
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_sctp.h#L8
+SCTP_CONNTRACK_CLOSED = 1
+SCTP_CONNTRACK_COOKIE_WAIT = 2
+SCTP_CONNTRACK_COOKIE_ECHOED = 3
+SCTP_CONNTRACK_ESTABLISHED = 4
+SCTP_CONNTRACK_SHUTDOWN_SENT = 5
+SCTP_CONNTRACK_SHUTDOWN_RECD = 6
+SCTP_CONNTRACK_SHUTDOWN_ACK_SENT = 7
+SCTP_CONNTRACK_HEARTBEAT_SENT = 8
+SCTP_CONNTRACK_HEARTBEAT_ACKED = 9 # no longer used
+SCTP_CONNTRACK_MAX = 10
+
+SCTP_CONNTRACK_TO_NAME = {
+ SCTP_CONNTRACK_CLOSED: 'CLOSED',
+ SCTP_CONNTRACK_COOKIE_WAIT: 'COOKIE_WAIT',
+ SCTP_CONNTRACK_COOKIE_ECHOED: 'COOKIE_ECHOED',
+ SCTP_CONNTRACK_ESTABLISHED: 'ESTABLISHED',
+ SCTP_CONNTRACK_SHUTDOWN_SENT: 'SHUTDOWN_SENT',
+ SCTP_CONNTRACK_SHUTDOWN_RECD: 'SHUTDOWN_RECD',
+ SCTP_CONNTRACK_SHUTDOWN_ACK_SENT: 'SHUTDOWN_ACK_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_SENT: 'HEARTBEAT_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_ACKED: 'HEARTBEAT_ACKED',
+ SCTP_CONNTRACK_MAX: 'MAX',
+}
+
+PROTO_CONNTRACK_TO_NAME = {
+ 'TCP': TCP_CONNTRACK_TO_NAME,
+ 'SCTP': SCTP_CONNTRACK_TO_NAME
+}
+
+SUPPORTED_PROTO_TO_NAME = {
+ socket.IPPROTO_ICMP: 'icmp',
+ socket.IPPROTO_TCP: 'tcp',
+ socket.IPPROTO_UDP: 'udp',
+}
+
+PROTO_TO_NAME = {
+ socket.IPPROTO_ICMPV6: 'icmpv6',
+ socket.IPPROTO_SCTP: 'sctp',
+ socket.IPPROTO_GRE: 'gre',
+}
+
+PROTO_TO_NAME.update(SUPPORTED_PROTO_TO_NAME)
+
+
+def sig_handler(signum, frame):
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}]: {"Shutdown" if signum == signal.SIGTERM else "Reload"} signal received...')
+ shutdown_event.set()
+
+
+def format_flow_data(data: Dict) -> AnyStr:
+ """
+ Formats the flow event data into a string suitable for logging.
+ """
+ key_format = {
+ 'SRC_PORT': 'sport',
+ 'DST_PORT': 'dport'
+ }
+ message = f"src={data['ADDR'].get('SRC')} dst={data['ADDR'].get('DST')}"
+
+ for key in ['SRC_PORT', 'DST_PORT', 'TYPE', 'CODE', 'ID']:
+ tmp = data['PROTO'].get(key)
+ if tmp is not None:
+ key = key_format.get(key, key)
+ message += f" {key.lower()}={tmp}"
+
+ if 'COUNTERS' in data:
+ for key in ['PACKETS', 'BYTES']:
+ tmp = data['COUNTERS'].get(key)
+ if tmp is not None:
+ message += f" {key.lower()}={tmp}"
+
+ return message
+
+
+def format_event_message(event: Dict) -> AnyStr:
+ """
+ Formats the internal parsed event data into a string suitable for logging.
+ """
+ event_type = f"[{event['COMMON']['EVENT_TYPE'].upper()}]"
+ message = f"{event_type:<{9}} {event['COMMON']['ID']} " \
+ f"{event['ORIG']['PROTO'].get('NAME'):<{8}} " \
+ f"{event['ORIG']['PROTO'].get('NUMBER')} "
+
+ tmp = event['COMMON']['TIME_OUT']
+ if tmp is not None: message += f"{tmp} "
+
+ if proto_info := event['COMMON'].get('PROTO_INFO'):
+ message += f"{proto_info.get('STATE_NAME')} "
+
+ for key in ['ORIG', 'REPLY']:
+ message += f"{format_flow_data(event[key])} "
+ if key == 'ORIG' and not (event['COMMON']['STATUS'] & IPS_SEEN_REPLY):
+ message += f"[UNREPLIED] "
+
+ tmp = event['COMMON']['MARK']
+ if tmp is not None: message += f"mark={tmp} "
+
+ if event['COMMON']['STATUS'] & IPS_OFFLOAD: message += f" [OFFLOAD] "
+ elif event['COMMON']['STATUS'] & IPS_ASSURED: message += f" [ASSURED] "
+
+ if tmp := event['COMMON']['PORTID']: message += f"portid={tmp} "
+ if tstamp := event['COMMON'].get('TIMESTAMP'):
+ message += f"start={tstamp['START']} stop={tstamp['STOP']} "
+ delta_ns = tstamp['STOP'] - tstamp['START']
+ delta_s = delta_ns // 1e9
+ remaining_ns = delta_ns % 1e9
+ delta = timedelta(seconds=delta_s, microseconds=remaining_ns / 1000)
+ message += f"delta={delta.total_seconds()} "
+
+ return message
+
+
+def parse_event_type(header: Dict) -> AnyStr:
+ """
+ Extract event type from nfct_msg. new, update, destroy
+ """
+ event_type = 'unknown'
+ if header['type'] == IPCTNL_MSG_CT_DELETE | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'destroy'
+ elif header['type'] == IPCTNL_MSG_CT_NEW | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'update'
+ if header['flags']:
+ event_type = 'new'
+ return event_type
+
+
+def parse_proto(cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract proto info from nfct_msg. src/dst port, code, type, id
+ """
+ data = dict()
+
+ cta_proto = cta.get_attr('CTA_TUPLE_PROTO')
+ proto_num = cta_proto.get_attr('CTA_PROTO_NUM')
+
+ data['NUMBER'] = proto_num
+ data['NAME'] = PROTO_TO_NAME.get(proto_num, 'unknown')
+
+ if proto_num in (socket.IPPROTO_ICMP, socket.IPPROTO_ICMPV6):
+ pref = 'CTA_PROTO_ICMP'
+ if proto_num == socket.IPPROTO_ICMPV6: pref += 'V6'
+ keys = ['TYPE', 'CODE', 'ID']
+ else:
+ pref = 'CTA_PROTO'
+ keys = ['SRC_PORT', 'DST_PORT']
+
+ for key in keys:
+ data[key] = cta_proto.get_attr(f'{pref}_{key}')
+
+ return data
+
+
+def parse_proto_info(cta: nfct_msg.cta_protoinfo) -> Dict:
+ """
+ Extract proto state and state name from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for proto in ['TCP', 'SCTP']:
+ if proto_info := cta.get_attr(f'CTA_PROTOINFO_{proto}'):
+ data['STATE'] = proto_info.get_attr(f'CTA_PROTOINFO_{proto}_STATE')
+ data['STATE_NAME'] = PROTO_CONNTRACK_TO_NAME.get(proto, {}).get(data['STATE'], 'unknown')
+ return data
+
+
+def parse_timestamp(cta: nfct_msg.cta_timestamp) -> Dict:
+ """
+ Extract timestamp from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+ data['START'] = cta.get_attr('CTA_TIMESTAMP_START')
+ data['STOP'] = cta.get_attr('CTA_TIMESTAMP_STOP')
+
+ return data
+
+
+def parse_ip_addr(family: int, cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract ip adr from nfct_msg
+ """
+ data = dict()
+ cta_ip = cta.get_attr('CTA_TUPLE_IP')
+
+ if family == socket.AF_INET:
+ pref = 'CTA_IP_V4'
+ elif family == socket.AF_INET6:
+ pref = 'CTA_IP_V6'
+ else:
+ logger.error(f'Undefined INET: {family}')
+ raise NotImplementedError(family)
+
+ for direct in ['SRC', 'DST']:
+ data[direct] = cta_ip.get_attr(f'{pref}_{direct}')
+
+ return data
+
+
+def parse_counters(cta: nfct_msg.cta_counters) -> Dict:
+ """
+ Extract counters from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for key in ['PACKETS', 'BYTES']:
+ tmp = cta.get_attr(f'CTA_COUNTERS_{key}')
+ if tmp is None:
+ tmp = cta.get_attr(f'CTA_COUNTERS32_{key}')
+ data['key'] = tmp
+
+ return data
+
+
+def is_need_to_log(event_type: AnyStr, proto_num: int, conf_event: Dict):
+ """
+ Filter message by event type and protocols
+ """
+ conf = conf_event.get(event_type)
+ if conf == {} or conf.get(SUPPORTED_PROTO_TO_NAME.get(proto_num, 'other')) is not None:
+ return True
+ return False
+
+
+def parse_conntrack_event(msg: nfct_msg, conf_event: Dict) -> Dict:
+ """
+ Convert nfct_msg to internal data dict.
+ """
+ data = dict()
+ event_type = parse_event_type(msg['header'])
+ proto_num = msg.get_nested('CTA_TUPLE_ORIG', 'CTA_TUPLE_PROTO', 'CTA_PROTO_NUM')
+
+ if not is_need_to_log(event_type, proto_num, conf_event):
+ return data
+
+ data = {
+ 'COMMON': {
+ 'ID': msg.get_attr('CTA_ID'),
+ 'EVENT_TYPE': event_type,
+ 'TIME_OUT': msg.get_attr('CTA_TIMEOUT'),
+ 'MARK': msg.get_attr('CTA_MARK'),
+ 'PORTID': msg['header'].get('pid'),
+ 'PROTO_INFO': parse_proto_info(msg.get_attr('CTA_PROTOINFO')),
+ 'STATUS': msg.get_attr('CTA_STATUS'),
+ 'TIMESTAMP': parse_timestamp(msg.get_attr('CTA_TIMESTAMP'))
+ },
+ 'ORIG': {},
+ 'REPLY': {},
+ }
+
+ for direct in ['ORIG', 'REPLY']:
+ data[direct]['ADDR'] = parse_ip_addr(msg['nfgen_family'], msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['PROTO'] = parse_proto(msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['COUNTERS'] = parse_counters(msg.get_attr(f'CTA_COUNTERS_{direct}'))
+
+ return data
+
+
+def worker(ct: conntrack.Conntrack, shutdown_event: multiprocessing.Event, conf_event: Dict):
+ """
+ Main function of parser worker process
+ """
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}] started')
+ timeout = 0.1
+ while not shutdown_event.is_set():
+ if not ct.buffer_queue.empty():
+ try:
+ for msg in ct.get():
+ parsed_event = parse_conntrack_event(msg, conf_event)
+ if parsed_event:
+ message = format_event_message(parsed_event)
+ if logger.level == logging.DEBUG:
+ logger.debug(f"[{process_name}]: {message} raw: {msg}")
+ else:
+ logger.info(message)
+ except queue.Full:
+ logger.error("Conntrack message queue if full.")
+ except Exception as e:
+ logger.error(f"Error in queue: {e.__class__} {e}")
+ else:
+ sleep(timeout)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to vyos-conntrack-logger configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config = read_json(args.config)
+ except Exception as err:
+ logger.error(f'Configuration file "{args.config}" does not exist or malformed: {err}')
+ exit(1)
+
+ set_log_level(config.get('log_level', 'info'))
+
+ signal.signal(signal.SIGHUP, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ if 'event' in config:
+ event_groups = list(config.get('event').keys())
+ else:
+ logger.error(f'Configuration is wrong. Event filter is empty.')
+ exit(1)
+
+ conf_event = config['event']
+ qsize = config.get('queue_size')
+ ct = conntrack.Conntrack(async_qsize=int(qsize) if qsize else None)
+ ct.buffer_queue = multiprocessing.Queue(ct.async_qsize)
+ ct.bind(async_cache=True)
+
+ for name in event_groups:
+ if group := EVENT_NAME_TO_GROUP.get(name):
+ ct.add_membership(group)
+ else:
+ logger.error(f'Unexpected event group {name}')
+ processes = list()
+ try:
+ for _ in range(multiprocessing.cpu_count()):
+ p = multiprocessing.Process(target=worker, args=(ct,
+ shutdown_event,
+ conf_event))
+ processes.append(p)
+ p.start()
+ logger.info('Conntrack socket bound and listening for messages.')
+
+ while not shutdown_event.is_set():
+ if not ct.pthread.is_alive():
+ if ct.buffer_queue.qsize()/ct.async_qsize < 0.9:
+ if not shutdown_event.is_set():
+ logger.debug('Restart listener thread')
+ # restart listener thread after queue overloaded when queue size low than 90%
+ ct.pthread = threading.Thread(
+ name="Netlink async cache", target=ct.async_recv
+ )
+ ct.pthread.daemon = True
+ ct.pthread.start()
+ else:
+ sleep(0.1)
+ finally:
+ for p in processes:
+ p.join()
+ if not p.is_alive():
+ logger.debug(f"[{p.name}]: finished")
+ ct.close()
+ logging.info("Conntrack socket closed.")
+ exit()
diff --git a/src/systemd/vyos-conntrack-logger.service b/src/systemd/vyos-conntrack-logger.service
new file mode 100644
index 000000000..9bc1d857b
--- /dev/null
+++ b/src/systemd/vyos-conntrack-logger.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=VyOS conntrack logger daemon
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=conntrackd.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-conntrack-logger -c /run/vyos-conntrack-logger.conf
+Type=idle
+
+SyslogIdentifier=vyos-conntrack-logger
+SyslogFacility=daemon
+
+Restart=on-failure
+
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target