summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--data/config-mode-dependencies/vyos-1x.json3
-rw-r--r--data/templates/firewall/nftables-zone.j269
-rw-r--r--data/templates/firewall/nftables.j238
-rw-r--r--data/templates/frr/ldpd.frr.j210
-rw-r--r--data/templates/https/vyos-http-api.service.j21
-rw-r--r--data/templates/openvpn/server.conf.j29
-rw-r--r--data/templates/pmacct/override.conf.j24
-rw-r--r--data/templates/pmacct/uacctd.conf.j22
-rw-r--r--git0
-rw-r--r--interface-definitions/firewall.xml.in142
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-forward.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-input.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-output.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-forward.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-input.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-output.xml.i1
-rw-r--r--interface-definitions/include/version/cluster-version.xml.i2
-rw-r--r--interface-definitions/include/version/interfaces-version.xml.i2
-rw-r--r--interface-definitions/include/version/openvpn-version.xml.i3
-rw-r--r--interface-definitions/interfaces-bridge.xml.in3
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in24
-rw-r--r--interface-definitions/interfaces-vxlan.xml.in2
-rw-r--r--interface-definitions/policy-local-route.xml.in5
-rw-r--r--interface-definitions/xml-component-version.xml.in1
-rw-r--r--op-mode-definitions/generate_tech-support_archive.xml.in29
-rw-r--r--op-mode-definitions/show-interfaces-wireless.xml.in10
-rw-r--r--op-mode-definitions/show-ssh.xml.in28
-rw-r--r--python/vyos/configdep.py2
-rw-r--r--python/vyos/configdict.py14
-rw-r--r--python/vyos/ifconfig/bond.py13
-rw-r--r--python/vyos/ifconfig/ethernet.py34
-rw-r--r--python/vyos/nat.py2
-rw-r--r--python/vyos/progressbar.py70
-rw-r--r--python/vyos/qos/trafficshaper.py9
-rw-r--r--python/vyos/remote.py44
-rw-r--r--python/vyos/template.py3
-rw-r--r--python/vyos/utils/dict.py59
-rw-r--r--python/vyos/utils/io.py39
-rwxr-xr-xscripts/import-conf-mode-commands255
-rw-r--r--smoketest/configs/bgp-evpn-l2vpn-leaf1
-rw-r--r--smoketest/configs/cluster-basic62
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py66
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py4
-rwxr-xr-xsmoketest/scripts/cli/test_policy.py50
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py6
-rwxr-xr-xsmoketest/scripts/cli/test_qos.py57
-rwxr-xr-xsrc/conf_mode/firewall.py70
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py34
-rwxr-xr-xsrc/conf_mode/http-api.py6
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py38
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py237
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py7
-rwxr-xr-xsrc/conf_mode/policy-local-route.py79
-rwxr-xr-xsrc/migration-scripts/cluster/1-to-2193
-rwxr-xr-xsrc/migration-scripts/firewall/10-to-11185
-rwxr-xr-xsrc/migration-scripts/interfaces/30-to-3171
-rwxr-xr-xsrc/migration-scripts/interfaces/31-to-3246
-rwxr-xr-xsrc/migration-scripts/openvpn/0-to-149
-rwxr-xr-xsrc/op_mode/generate_tech-support_archive.py148
-rwxr-xr-xsrc/op_mode/interfaces_wireless.py186
-rwxr-xr-xsrc/op_mode/lldp.py5
-rw-r--r--src/op_mode/show-ssh-fingerprints.py49
-rwxr-xr-xsrc/op_mode/show_wireless.py149
-rwxr-xr-xsrc/services/vyos-http-api-server173
-rwxr-xr-xsrc/system/uacctd_stop.py68
67 files changed, 2161 insertions, 821 deletions
diff --git a/Makefile b/Makefile
index 97f4de6c7..9a30ab74e 100644
--- a/Makefile
+++ b/Makefile
@@ -26,10 +26,10 @@ interface_definitions: $(config_xml_obj)
$(CURDIR)/scripts/override-default $(BUILD_DIR)/interface-definitions
- $(CURDIR)/python/vyos/xml_ref/generate_cache.py --xml-dir $(BUILD_DIR)/interface-definitions || exit 1
-
find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1
+ $(CURDIR)/python/vyos/xml_ref/generate_cache.py --xml-dir $(BUILD_DIR)/interface-definitions || exit 1
+
# XXX: delete top level node.def's that now live in other packages
# IPSec VPN EAP-RADIUS does not support source-address
rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address
@@ -60,7 +60,6 @@ op_mode_definitions: $(op_xml_obj)
rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def
rm -f $(OP_TMPL_DIR)/clear/node.def
rm -f $(OP_TMPL_DIR)/delete/node.def
- rm -f $(OP_TMPL_DIR)/generate/node.def
rm -f $(OP_TMPL_DIR)/set/node.def
# XXX: ping and traceroute must be able to recursivly call itself as the
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json
index 6c86642c7..4d73c844c 100644
--- a/data/config-mode-dependencies/vyos-1x.json
+++ b/data/config-mode-dependencies/vyos-1x.json
@@ -6,6 +6,9 @@
"group_resync": ["conntrack", "nat", "policy-route"]
},
"http_api": {"https": ["https"]},
+ "interfaces_bonding": {
+ "ethernet": ["interfaces-ethernet"]
+ },
"load_balancing_wan": {
"conntrack": ["conntrack"],
"conntrack_sync": ["conntrack_sync"]
diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2
new file mode 100644
index 000000000..1e9351f97
--- /dev/null
+++ b/data/templates/firewall/nftables-zone.j2
@@ -0,0 +1,69 @@
+
+{% macro zone_chains(zone, ipv6=False) %}
+{% set fw_name = 'ipv6_name' if ipv6 else 'name' %}
+{% set suffix = '6' if ipv6 else '' %}
+ chain VYOS_ZONE_FORWARD {
+ type filter hook forward priority 1; policy accept;
+{% for zone_name, zone_conf in zone.items() %}
+{% if 'local_zone' not in zone_conf %}
+ oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }}
+{% endif %}
+{% endfor %}
+ }
+ chain VYOS_ZONE_LOCAL {
+ type filter hook input priority 1; policy accept;
+{% for zone_name, zone_conf in zone.items() %}
+{% if 'local_zone' in zone_conf %}
+ counter jump VZONE_{{ zone_name }}_IN
+{% endif %}
+{% endfor %}
+ }
+ chain VYOS_ZONE_OUTPUT {
+ type filter hook output priority 1; policy accept;
+{% for zone_name, zone_conf in zone.items() %}
+{% if 'local_zone' in zone_conf %}
+ counter jump VZONE_{{ zone_name }}_OUT
+{% endif %}
+{% endfor %}
+ }
+{% for zone_name, zone_conf in zone.items() %}
+{% if zone_conf.local_zone is vyos_defined %}
+ chain VZONE_{{ zone_name }}_IN {
+ iifname lo counter return
+{% if zone_conf.from is vyos_defined %}
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+{% endif %}
+ {{ zone_conf | nft_default_rule('zone_' + zone_name) }}
+ }
+ chain VZONE_{{ zone_name }}_OUT {
+ oifname lo counter return
+{% if zone_conf.from_local is vyos_defined %}
+{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall[fw_name] is vyos_defined %}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+{% endif %}
+ {{ zone_conf | nft_default_rule('zone_' + zone_name) }}
+ }
+{% else %}
+ chain VZONE_{{ zone_name }} {
+ iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6) }}
+{% if zone_conf.intra_zone_filtering is vyos_defined %}
+ iifname { {{ zone_conf.interface | join(",") }} } counter return
+{% endif %}
+{% if zone_conf.from is vyos_defined %}
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall[fw_name] is vyos_defined %}
+{% if zone[from_zone].local_zone is not defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME{{ suffix }}_{{ from_conf.firewall[fw_name] }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endif %}
+{% endfor %}
+{% endif %}
+ {{ zone_conf | nft_default_rule('zone_' + zone_name) }}
+ }
+{% endif %}
+{% endfor %}
+{% endmacro %} \ No newline at end of file
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index 75800ee3d..e24a9655d 100644
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -3,6 +3,7 @@
{% import 'firewall/nftables-defines.j2' as group_tmpl %}
{% import 'firewall/nftables-bridge.j2' as bridge_tmpl %}
{% import 'firewall/nftables-offload.j2' as offload_tmpl %}
+{% import 'firewall/nftables-zone.j2' as zone_tmpl %}
flush chain raw vyos_global_rpfilter
flush chain ip6 raw vyos_global_rpfilter
@@ -43,9 +44,8 @@ table ip vyos_filter {
{% set ns = namespace(sets=[]) %}
{% if ipv4.forward is vyos_defined %}
{% for prior, conf in ipv4.forward.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_FORWARD_{{ prior }} {
- type filter hook forward priority {{ prior }}; policy {{ def_action }};
+ type filter hook forward priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('FWD', prior, rule_id) }}
@@ -54,15 +54,15 @@ table ip vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('FWD-filter') }}
}
{% endfor %}
{% endif %}
{% if ipv4.input is vyos_defined %}
{% for prior, conf in ipv4.input.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_INPUT_{{ prior }} {
- type filter hook input priority {{ prior }}; policy {{ def_action }};
+ type filter hook input priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('INP',prior, rule_id) }}
@@ -71,15 +71,15 @@ table ip vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('INP-filter') }}
}
{% endfor %}
{% endif %}
{% if ipv4.output is vyos_defined %}
{% for prior, conf in ipv4.output.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_OUTPUT_{{ prior }} {
- type filter hook output priority {{ prior }}; policy {{ def_action }};
+ type filter hook output priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('OUT', prior, rule_id) }}
@@ -88,6 +88,7 @@ table ip vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('OUT-filter') }}
}
{% endfor %}
{% endif %}
@@ -97,9 +98,8 @@ table ip vyos_filter {
}
{% if ipv4.prerouting is vyos_defined %}
{% for prior, conf in ipv4.prerouting.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_PREROUTING_{{ prior }} {
- type filter hook prerouting priority {{ prior }}; policy {{ def_action }};
+ type filter hook prerouting priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('PRE', prior, rule_id) }}
@@ -108,7 +108,7 @@ table ip vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
- {{ conf | nft_default_rule(prior) }}
+ {{ conf | nft_default_rule('PRE-filter') }}
}
{% endfor %}
{% endif %}
@@ -152,6 +152,10 @@ table ip vyos_filter {
{% endif %}
{% endif %}
{{ group_tmpl.groups(group, False, True) }}
+
+{% if zone is vyos_defined %}
+{{ zone_tmpl.zone_chains(zone, False) }}
+{% endif %}
}
{% if first_install is not vyos_defined %}
@@ -168,9 +172,8 @@ table ip6 vyos_filter {
{% set ns = namespace(sets=[]) %}
{% if ipv6.forward is vyos_defined %}
{% for prior, conf in ipv6.forward.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_IPV6_FORWARD_{{ prior }} {
- type filter hook forward priority {{ prior }}; policy {{ def_action }};
+ type filter hook forward priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('FWD', prior, rule_id ,'ip6') }}
@@ -179,15 +182,15 @@ table ip6 vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('FWD-filter', ipv6=True) }}
}
{% endfor %}
{% endif %}
{% if ipv6.input is vyos_defined %}
{% for prior, conf in ipv6.input.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_IPV6_INPUT_{{ prior }} {
- type filter hook input priority {{ prior }}; policy {{ def_action }};
+ type filter hook input priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('INP', prior, rule_id ,'ip6') }}
@@ -196,15 +199,15 @@ table ip6 vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('INP-filter', ipv6=True) }}
}
{% endfor %}
{% endif %}
{% if ipv6.output is vyos_defined %}
{% for prior, conf in ipv6.output.items() %}
-{% set def_action = conf.default_action %}
chain VYOS_IPV6_OUTPUT_{{ prior }} {
- type filter hook output priority {{ prior }}; policy {{ def_action }};
+ type filter hook output priority {{ prior }}; policy accept;
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
{{ rule_conf | nft_rule('OUT', prior, rule_id ,'ip6') }}
@@ -213,6 +216,7 @@ table ip6 vyos_filter {
{% endif %}
{% endfor %}
{% endif %}
+ {{ conf | nft_default_rule('OUT-filter', ipv6=True) }}
}
{% endfor %}
{% endif %}
@@ -261,6 +265,9 @@ table ip6 vyos_filter {
{% endif %}
{% endif %}
{{ group_tmpl.groups(group, True, True) }}
+{% if zone is vyos_defined %}
+{{ zone_tmpl.zone_chains(zone, True) }}
+{% endif %}
}
## Bridge Firewall
@@ -270,4 +277,5 @@ delete table bridge vyos_filter
table bridge vyos_filter {
{{ bridge_tmpl.bridge(bridge) }}
{{ group_tmpl.groups(group, False, False) }}
+
}
diff --git a/data/templates/frr/ldpd.frr.j2 b/data/templates/frr/ldpd.frr.j2
index 11aff331a..9a893cc55 100644
--- a/data/templates/frr/ldpd.frr.j2
+++ b/data/templates/frr/ldpd.frr.j2
@@ -14,19 +14,19 @@ mpls ldp
ordered-control
{% endif %}
{% if ldp.neighbor is vyos_defined %}
-{% for neighbor, neighbor_config in ldp.neighbor %}
+{% for neighbor, neighbor_config in ldp.neighbor.items() %}
{% if neighbor_config.password is vyos_defined %}
- neighbor {{ neighbors }} password {{ neighbor_config.password }}
+ neighbor {{ neighbor }} password {{ neighbor_config.password }}
{% endif %}
{% if neighbor_config.ttl_security is vyos_defined %}
{% if neighbor_config.ttl_security.disable is vyos_defined %}
- neighbor {{ neighbors }} ttl-security disable
+ neighbor {{ neighbor }} ttl-security disable
{% else %}
- neighbor {{ neighbors }} ttl-security hops {{ neighbor_config.ttl_security }}
+ neighbor {{ neighbor }} ttl-security hops {{ neighbor_config.ttl_security }}
{% endif %}
{% endif %}
{% if neighbor_config.session_holdtime is vyos_defined %}
- neighbor {{ neighbors }} session holdtime {{ neighbor_config.session_holdtime }}
+ neighbor {{ neighbor }} session holdtime {{ neighbor_config.session_holdtime }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/https/vyos-http-api.service.j2 b/data/templates/https/vyos-http-api.service.j2
index fb424e06c..f620b3248 100644
--- a/data/templates/https/vyos-http-api.service.j2
+++ b/data/templates/https/vyos-http-api.service.j2
@@ -6,6 +6,7 @@ Requires=vyos-router.service
[Service]
ExecStart={{ vrf_command }}/usr/libexec/vyos/services/vyos-http-api-server
+ExecReload=kill -HUP $MAINPID
Type=idle
SyslogIdentifier=vyos-http-api
diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2
index 2eb9416fe..746155c37 100644
--- a/data/templates/openvpn/server.conf.j2
+++ b/data/templates/openvpn/server.conf.j2
@@ -205,19 +205,12 @@ tls-server
{% if encryption is vyos_defined %}
{% if encryption.cipher is vyos_defined %}
cipher {{ encryption.cipher | openvpn_cipher }}
-{% if encryption.cipher is vyos_defined('bf128') %}
-keysize 128
-{% elif encryption.cipher is vyos_defined('bf256') %}
-keysize 256
-{% endif %}
{% endif %}
{% if encryption.ncp_ciphers is vyos_defined %}
data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }}
{% endif %}
{% endif %}
-# https://vyos.dev/T5027
-# Required to support BF-CBC (default ciphername when none given)
-providers legacy default
+providers default
{% if hash is vyos_defined %}
auth {{ hash }}
diff --git a/data/templates/pmacct/override.conf.j2 b/data/templates/pmacct/override.conf.j2
index 213569ddc..44a100bb6 100644
--- a/data/templates/pmacct/override.conf.j2
+++ b/data/templates/pmacct/override.conf.j2
@@ -9,9 +9,9 @@ ConditionPathExists=/run/pmacct/uacctd.conf
EnvironmentFile=
ExecStart=
ExecStart={{ vrf_command }}/usr/sbin/uacctd -f /run/pmacct/uacctd.conf
+ExecStop=/usr/libexec/vyos/system/uacctd_stop.py $MAINPID 60
WorkingDirectory=
WorkingDirectory=/run/pmacct
-PIDFile=
-PIDFile=/run/pmacct/uacctd.pid
Restart=always
RestartSec=10
+KillMode=mixed
diff --git a/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2
index 1370f8121..aae0a0619 100644
--- a/data/templates/pmacct/uacctd.conf.j2
+++ b/data/templates/pmacct/uacctd.conf.j2
@@ -1,7 +1,7 @@
# Genereated from VyOS configuration
daemonize: true
promisc: false
-pidfile: /run/pmacct/uacctd.pid
+syslog: daemon
uacctd_group: 2
uacctd_nl_size: 2097152
snaplen: {{ packet_length }}
diff --git a/git b/git
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/git
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index 81e6b89ea..0bb14a1b3 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -355,6 +355,148 @@
#include <include/firewall/ipv6-custom-name.xml.i>
</children>
</node>
+ <tagNode name="zone">
+ <properties>
+ <help>Zone-policy</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Zone name</description>
+ </valueHelp>
+ <constraint>
+ <regex>[a-zA-Z0-9][\w\-\.]*</regex>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
+ <leafNode name="default-action">
+ <properties>
+ <help>Default-action for traffic coming into this zone</help>
+ <completionHelp>
+ <list>drop reject</list>
+ </completionHelp>
+ <valueHelp>
+ <format>drop</format>
+ <description>Drop silently</description>
+ </valueHelp>
+ <valueHelp>
+ <format>reject</format>
+ <description>Drop and notify source</description>
+ </valueHelp>
+ <constraint>
+ <regex>(drop|reject)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>drop</defaultValue>
+ </leafNode>
+ <tagNode name="from">
+ <properties>
+ <help>Zone from which to filter traffic</help>
+ <completionHelp>
+ <path>zone-policy zone</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="firewall">
+ <properties>
+ <help>Firewall options</help>
+ </properties>
+ <children>
+ <leafNode name="ipv6-name">
+ <properties>
+ <help>IPv6 firewall ruleset</help>
+ <completionHelp>
+ <path>firewall ipv6 name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="name">
+ <properties>
+ <help>IPv4 firewall ruleset</help>
+ <completionHelp>
+ <path>firewall ipv4 name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface associated with zone</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface associated with zone</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vrf</format>
+ <description>VRF associated with zone</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ <path>vrf name</path>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="intra-zone-filtering">
+ <properties>
+ <help>Intra-zone filtering</help>
+ </properties>
+ <children>
+ <leafNode name="action">
+ <properties>
+ <help>Action for intra-zone traffic</help>
+ <completionHelp>
+ <list>accept drop</list>
+ </completionHelp>
+ <valueHelp>
+ <format>accept</format>
+ <description>Accept traffic</description>
+ </valueHelp>
+ <valueHelp>
+ <format>drop</format>
+ <description>Drop silently</description>
+ </valueHelp>
+ <constraint>
+ <regex>(accept|drop)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="firewall">
+ <properties>
+ <help>Use the specified firewall chain</help>
+ </properties>
+ <children>
+ <leafNode name="ipv6-name">
+ <properties>
+ <help>IPv6 firewall ruleset</help>
+ <completionHelp>
+ <path>firewall ipv6 name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="name">
+ <properties>
+ <help>IPv4 firewall ruleset</help>
+ <completionHelp>
+ <path>firewall ipv4 name</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <leafNode name="local-zone">
+ <properties>
+ <help>Zone to be local-zone</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
</node>
</interfaceDefinition>
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 504385b53..3d9333639 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -343,6 +343,7 @@
#include <include/route-map.xml.i>
</children>
</tagNode>
+ #include <include/bgp/afi-maximum-paths.xml.i>
</children>
</node>
<node name="ipv4-flowspec">
diff --git a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
index 70c0adb77..100f1c3d9 100644
--- a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/firewall/ipv4-hook-input.xml.i b/interface-definitions/include/firewall/ipv4-hook-input.xml.i
index 32b0ec94f..22546640b 100644
--- a/interface-definitions/include/firewall/ipv4-hook-input.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-input.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/firewall/ipv4-hook-output.xml.i b/interface-definitions/include/firewall/ipv4-hook-output.xml.i
index d50d1e93b..80c30cdeb 100644
--- a/interface-definitions/include/firewall/ipv4-hook-output.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-output.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
index d83827161..fb38267eb 100644
--- a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i
index e34958f28..49d4493cc 100644
--- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/firewall/ipv6-hook-output.xml.i b/interface-definitions/include/firewall/ipv6-hook-output.xml.i
index eb4ea7ac3..452b9027f 100644
--- a/interface-definitions/include/firewall/ipv6-hook-output.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-output.xml.i
@@ -10,6 +10,7 @@
</properties>
<children>
#include <include/firewall/default-action-base-chains.xml.i>
+ #include <include/firewall/enable-default-log.xml.i>
#include <include/generic-description.xml.i>
<tagNode name="rule">
<properties>
diff --git a/interface-definitions/include/version/cluster-version.xml.i b/interface-definitions/include/version/cluster-version.xml.i
index 621996df4..402fe36c5 100644
--- a/interface-definitions/include/version/cluster-version.xml.i
+++ b/interface-definitions/include/version/cluster-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/cluster-version.xml.i -->
-<syntaxVersion component='cluster' version='1'></syntaxVersion>
+<syntaxVersion component='cluster' version='2'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i
index 3d11ce888..854e60f4e 100644
--- a/interface-definitions/include/version/interfaces-version.xml.i
+++ b/interface-definitions/include/version/interfaces-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/interfaces-version.xml.i -->
-<syntaxVersion component='interfaces' version='30'></syntaxVersion>
+<syntaxVersion component='interfaces' version='32'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/openvpn-version.xml.i b/interface-definitions/include/version/openvpn-version.xml.i
new file mode 100644
index 000000000..b4dd742a3
--- /dev/null
+++ b/interface-definitions/include/version/openvpn-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/openvpn-version.xml.i -->
+<syntaxVersion component='openvpn' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index fcfb8686c..db3762065 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -123,6 +123,9 @@
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces --bridgeable</script>
</completionHelp>
+ <constraint>
+ #include <include/constraint/interface-name.xml.i>
+ </constraint>
</properties>
<children>
<leafNode name="native-vlan">
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 831659250..b8b04334c 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -48,29 +48,17 @@
<properties>
<help>Standard Data Encryption Algorithm</help>
<completionHelp>
- <list>none des 3des bf128 bf256 aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
</completionHelp>
<valueHelp>
<format>none</format>
<description>Disable encryption</description>
</valueHelp>
<valueHelp>
- <format>des</format>
- <description>DES algorithm</description>
- </valueHelp>
- <valueHelp>
<format>3des</format>
<description>DES algorithm with triple encryption</description>
</valueHelp>
<valueHelp>
- <format>bf128</format>
- <description>Blowfish algorithm with 128-bit key</description>
- </valueHelp>
- <valueHelp>
- <format>bf256</format>
- <description>Blowfish algorithm with 256-bit key</description>
- </valueHelp>
- <valueHelp>
<format>aes128</format>
<description>AES algorithm with 128-bit key CBC</description>
</valueHelp>
@@ -95,7 +83,7 @@
<description>AES algorithm with 256-bit key GCM</description>
</valueHelp>
<constraint>
- <regex>(none|des|3des|bf128|bf256|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
</constraint>
</properties>
</leafNode>
@@ -103,17 +91,13 @@
<properties>
<help>Cipher negotiation list for use in server or client mode</help>
<completionHelp>
- <list>none des 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
</completionHelp>
<valueHelp>
<format>none</format>
<description>Disable encryption</description>
</valueHelp>
<valueHelp>
- <format>des</format>
- <description>DES algorithm</description>
- </valueHelp>
- <valueHelp>
<format>3des</format>
<description>DES algorithm with triple encryption</description>
</valueHelp>
@@ -142,7 +126,7 @@
<description>AES algorithm with 256-bit key GCM</description>
</valueHelp>
<constraint>
- <regex>(none|des|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
</constraint>
<multi/>
</properties>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index b246d9a09..3fae17178 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -93,7 +93,7 @@
</node>
#include <include/port-number.xml.i>
<leafNode name="port">
- <defaultValue>8472</defaultValue>
+ <defaultValue>4789</defaultValue>
</leafNode>
#include <include/source-address-ipv4-ipv6.xml.i>
#include <include/source-interface.xml.i>
diff --git a/interface-definitions/policy-local-route.xml.in b/interface-definitions/policy-local-route.xml.in
index 6827bd64e..15be099c9 100644
--- a/interface-definitions/policy-local-route.xml.in
+++ b/interface-definitions/policy-local-route.xml.in
@@ -60,6 +60,7 @@
</properties>
<children>
#include <include/policy/local-route_rule_ipv4_address.xml.i>
+ #include <include/port-number.xml.i>
</children>
</node>
<node name="destination">
@@ -68,6 +69,7 @@
</properties>
<children>
#include <include/policy/local-route_rule_ipv4_address.xml.i>
+ #include <include/port-number.xml.i>
</children>
</node>
#include <include/interface/inbound-interface.xml.i>
@@ -125,12 +127,14 @@
</constraint>
</properties>
</leafNode>
+ #include <include/policy/local-route_rule_protocol.xml.i>
<node name="source">
<properties>
<help>Source parameters</help>
</properties>
<children>
#include <include/policy/local-route_rule_ipv6_address.xml.i>
+ #include <include/port-number.xml.i>
</children>
</node>
<node name="destination">
@@ -139,6 +143,7 @@
</properties>
<children>
#include <include/policy/local-route_rule_ipv6_address.xml.i>
+ #include <include/port-number.xml.i>
</children>
</node>
#include <include/interface/inbound-interface.xml.i>
diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in
index 8c9e816d1..cae3423dc 100644
--- a/interface-definitions/xml-component-version.xml.in
+++ b/interface-definitions/xml-component-version.xml.in
@@ -19,6 +19,7 @@
#include <include/version/ids-version.xml.i>
#include <include/version/ipoe-server-version.xml.i>
#include <include/version/ipsec-version.xml.i>
+ #include <include/version/openvpn-version.xml.i>
#include <include/version/isis-version.xml.i>
#include <include/version/l2tp-version.xml.i>
#include <include/version/lldp-version.xml.i>
diff --git a/op-mode-definitions/generate_tech-support_archive.xml.in b/op-mode-definitions/generate_tech-support_archive.xml.in
new file mode 100644
index 000000000..e95be3e28
--- /dev/null
+++ b/op-mode-definitions/generate_tech-support_archive.xml.in
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="tech-support">
+ <properties>
+ <help>Generate tech support info</help>
+ </properties>
+ <children>
+ <node name="archive">
+ <properties>
+ <help>Generate tech support archive</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py</command>
+ </node>
+ <tagNode name="archive">
+ <properties>
+ <help>Generate tech support archive to defined location</help>
+ <completionHelp>
+ <list> &lt;file&gt; &lt;scp://user:passwd@host&gt; &lt;ftp://user:passwd@host&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-wireless.xml.in b/op-mode-definitions/show-interfaces-wireless.xml.in
index 27c0f43db..09c9a7895 100644
--- a/op-mode-definitions/show-interfaces-wireless.xml.in
+++ b/op-mode-definitions/show-interfaces-wireless.xml.in
@@ -20,7 +20,7 @@
<properties>
<help>Show wireless interface configuration</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_wireless.py --brief</command>
+ <command>${vyos_op_scripts_dir}/interfaces_wireless.py show_info</command>
</leafNode>
</children>
</node>
@@ -35,15 +35,15 @@
<children>
<leafNode name="brief">
<properties>
- <help>Show summary of the specified wireless interface information</help>
+ <help>Show brief summary of the specified wireless interface</help>
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=wireless</command>
</leafNode>
<node name="scan">
<properties>
- <help>Show summary of the specified wireless interface information</help>
+ <help>Scan for networks via specified wireless interface</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/show_wireless.py --scan "$4"</command>
+ <command>sudo ${vyos_op_scripts_dir}/interfaces_wireless.py show_scan --intf-name="$4"</command>
<children>
<leafNode name="detail">
<properties>
@@ -57,7 +57,7 @@
<properties>
<help>Show specified Wireless interface information</help>
</properties>
- <command>${vyos_op_scripts_dir}/show_wireless.py --stations "$4"</command>
+ <command>${vyos_op_scripts_dir}/interfaces_wireless.py show_stations --intf-name="$4"</command>
</leafNode>
<tagNode name="vif">
<properties>
diff --git a/op-mode-definitions/show-ssh.xml.in b/op-mode-definitions/show-ssh.xml.in
new file mode 100644
index 000000000..dc6e0d02e
--- /dev/null
+++ b/op-mode-definitions/show-ssh.xml.in
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="ssh">
+ <properties>
+ <help>Show SSH server information</help>
+ </properties>
+ <children>
+ <node name="fingerprints">
+ <properties>
+ <help>Show SSH server public key fingerprints</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show-ssh-fingerprints.py</command>
+ <children>
+ <node name="ascii">
+ <properties>
+ <help>Show visual ASCII art representation of the public key</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show-ssh-fingerprints.py --ascii</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index 05d9a3fa3..8a28811eb 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -43,7 +43,7 @@ def canon_name_of_path(path: str) -> str:
return canon_name(script)
def caller_name() -> str:
- return stack()[-1].filename
+ return stack()[2].filename
def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict:
res = {}
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 71a06b625..075ffe466 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -258,10 +258,10 @@ def has_address_configured(conf, intf):
old_level = conf.get_level()
conf.set_level([])
- intfpath = 'interfaces ' + Section.get_config_path(intf)
- if ( conf.exists(f'{intfpath} address') or
- conf.exists(f'{intfpath} ipv6 address autoconf') or
- conf.exists(f'{intfpath} ipv6 address eui64') ):
+ intfpath = ['interfaces', Section.get_config_path(intf)]
+ if (conf.exists([intfpath, 'address']) or
+ conf.exists([intfpath, 'ipv6', 'address', 'autoconf']) or
+ conf.exists([intfpath, 'ipv6', 'address', 'eui64'])):
ret = True
conf.set_level(old_level)
@@ -279,8 +279,7 @@ def has_vrf_configured(conf, intf):
old_level = conf.get_level()
conf.set_level([])
- tmp = ['interfaces', Section.get_config_path(intf), 'vrf']
- if conf.exists(tmp):
+ if conf.exists(['interfaces', Section.get_config_path(intf), 'vrf']):
ret = True
conf.set_level(old_level)
@@ -298,8 +297,7 @@ def has_vlan_subinterface_configured(conf, intf):
ret = False
intfpath = ['interfaces', Section.section(intf), intf]
- if ( conf.exists(intfpath + ['vif']) or
- conf.exists(intfpath + ['vif-s'])):
+ if (conf.exists(intfpath + ['vif']) or conf.exists(intfpath + ['vif-s'])):
ret = True
return ret
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index d1d7d48c4..45e6e4c16 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -92,6 +92,19 @@ class BondIf(Interface):
}
}}
+ @staticmethod
+ def get_inherit_bond_options() -> list:
+ """
+ Returns list of option
+ which are inherited from bond interface to member interfaces
+ :return: List of interface options
+ :rtype: list
+ """
+ options = [
+ 'mtu'
+ ]
+ return options
+
def remove(self):
"""
Remove interface from operating system. Removing the interface
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 285542057..aa1e87744 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -75,6 +75,40 @@ class EthernetIf(Interface):
},
}}
+ @staticmethod
+ def get_bond_member_allowed_options() -> list:
+ """
+ Return list of options which are allowed for changing,
+ when interface is a bond member
+ :return: List of interface options
+ :rtype: list
+ """
+ bond_allowed_sections = [
+ 'description',
+ 'disable',
+ 'disable_flow_control',
+ 'disable_link_detect',
+ 'duplex',
+ 'eapol.ca_certificate',
+ 'eapol.certificate',
+ 'eapol.passphrase',
+ 'mirror.egress',
+ 'mirror.ingress',
+ 'offload.gro',
+ 'offload.gso',
+ 'offload.lro',
+ 'offload.rfs',
+ 'offload.rps',
+ 'offload.sg',
+ 'offload.tso',
+ 'redirect',
+ 'ring_buffer.rx',
+ 'ring_buffer.tx',
+ 'speed',
+ 'hw_id'
+ ]
+ return bond_allowed_sections
+
def __init__(self, ifname, **kargs):
super().__init__(ifname, **kargs)
self.ethtool = Ethtool(ifname)
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 0887bfdf1..e32b5ae74 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -170,7 +170,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
operator = ''
if addr_prefix[:1] == '!':
operator = '!='
- addr_prefix = addr[1:]
+ addr_prefix = addr_prefix[1:]
output.append(f'ip6 {prefix}addr {operator} {addr_prefix}')
port = dict_search_args(side_conf, 'port')
diff --git a/python/vyos/progressbar.py b/python/vyos/progressbar.py
new file mode 100644
index 000000000..1793c445b
--- /dev/null
+++ b/python/vyos/progressbar.py
@@ -0,0 +1,70 @@
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import math
+import os
+import signal
+import subprocess
+import sys
+
+from vyos.utils.io import print_error
+
+class Progressbar:
+ def __init__(self, step=None):
+ self.total = 0.0
+ self.step = step
+ def __enter__(self):
+ # Recalculate terminal width with every window resize.
+ signal.signal(signal.SIGWINCH, lambda signum, frame: self._update_cols())
+ # Disable line wrapping to prevent the staircase effect.
+ subprocess.run(['tput', 'rmam'], check=False)
+ self._update_cols()
+ # Print an empty progressbar with entry.
+ self.progress(0, 1)
+ return self
+ def __exit__(self, exc_type, kexc_val, exc_tb):
+ # Revert to the default SIGWINCH handler (ie nothing).
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
+ # Reenable line wrapping.
+ subprocess.run(['tput', 'smam'], check=False)
+ def _update_cols(self):
+ # `os.get_terminal_size()' is fast enough for our purposes.
+ self.col = max(os.get_terminal_size().columns - 15, 20)
+ def increment(self):
+ """
+ Stateful progressbar taking the step fraction at init and no input at
+ callback (for FTP)
+ """
+ if self.step:
+ if self.total < 1.0:
+ self.total += self.step
+ if self.total >= 1.0:
+ self.total = 1.0
+ # Ignore superfluous calls caused by fuzzy FTP size calculations.
+ self.step = None
+ self.progress(self.total, 1.0)
+ def progress(self, done, total):
+ """
+ Stateless progressbar taking no input at init and current progress with
+ final size at callback (for SSH)
+ """
+ if done <= total:
+ length = math.ceil(self.col * done / total)
+ percentage = str(math.ceil(100 * done / total)).rjust(3)
+ # Carriage return at the end will make sure the line will get overwritten.
+ print_error(f'[{length * "#"}{(self.col - length) * "_"}] {percentage}%', end='\r')
+ # Print a newline to make sure the full progressbar doesn't get overwritten by the next line.
+ if done == total:
+ print_error()
diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py
index c63c7cf39..0d5f9a8a1 100644
--- a/python/vyos/qos/trafficshaper.py
+++ b/python/vyos/qos/trafficshaper.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2022-2023 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
@@ -89,6 +89,10 @@ class TrafficShaper(QoSBase):
if 'priority' in cls_config:
priority = cls_config['priority']
tmp += f' prio {priority}'
+
+ if 'ceiling' in cls_config:
+ f_ceil = self._rate_convert(cls_config['ceiling'])
+ tmp += f' ceil {f_ceil}'
self._cmd(tmp)
tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq'
@@ -102,6 +106,9 @@ class TrafficShaper(QoSBase):
if 'priority' in config['default']:
priority = config['default']['priority']
tmp += f' prio {priority}'
+ if 'ceiling' in config['default']:
+ f_ceil = self._rate_convert(config['default']['ceiling'])
+ tmp += f' ceil {f_ceil}'
self._cmd(tmp)
tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq'
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index cf731c881..1ca8a9530 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -32,9 +32,8 @@ from requests import Session
from requests.adapters import HTTPAdapter
from requests.packages.urllib3 import PoolManager
+from vyos.progressbar import Progressbar
from vyos.utils.io import ask_yes_no
-from vyos.utils.io import make_incremental_progressbar
-from vyos.utils.io import make_progressbar
from vyos.utils.io import print_error
from vyos.utils.misc import begin
from vyos.utils.process import cmd
@@ -131,16 +130,16 @@ class FtpC:
if self.secure:
conn.prot_p()
# Almost all FTP servers support the `SIZE' command.
+ size = conn.size(self.path)
if self.check_space:
- check_storage(path, conn.size(self.path))
+ check_storage(path, size)
# No progressbar if we can't determine the size or if the file is too small.
if self.progressbar and size and size > CHUNK_SIZE:
- progress = make_incremental_progressbar(CHUNK_SIZE / size)
- next(progress)
- callback = lambda block: begin(f.write(block), next(progress))
+ with Progressbar(CHUNK_SIZE / size) as p:
+ callback = lambda block: begin(f.write(block), p.increment())
+ conn.retrbinary('RETR ' + self.path, callback, CHUNK_SIZE)
else:
- callback = f.write
- conn.retrbinary('RETR ' + self.path, callback, CHUNK_SIZE)
+ conn.retrbinary('RETR ' + self.path, f.write, CHUNK_SIZE)
def upload(self, location: str):
size = os.path.getsize(location)
@@ -150,12 +149,10 @@ class FtpC:
if self.secure:
conn.prot_p()
if self.progressbar and size and size > CHUNK_SIZE:
- progress = make_incremental_progressbar(CHUNK_SIZE / size)
- next(progress)
- callback = lambda block: next(progress)
+ with Progressbar(CHUNK_SIZE / size) as p:
+ conn.storbinary('STOR ' + self.path, f, CHUNK_SIZE, lambda block: p.increment())
else:
- callback = None
- conn.storbinary('STOR ' + self.path, f, CHUNK_SIZE, callback)
+ conn.storbinary('STOR ' + self.path, f, CHUNK_SIZE)
class SshC:
known_hosts = os.path.expanduser('~/.ssh/known_hosts')
@@ -190,14 +187,16 @@ class SshC:
return ssh
def download(self, location: str):
- callback = make_progressbar() if self.progressbar else None
with self._establish() as ssh, ssh.open_sftp() as sftp:
if self.check_space:
check_storage(location, sftp.stat(self.path).st_size)
- sftp.get(self.path, location, callback=callback)
+ if self.progressbar:
+ with Progressbar() as p:
+ sftp.get(self.path, location, callback=p.progress)
+ else:
+ sftp.get(self.path, location)
def upload(self, location: str):
- callback = make_progressbar() if self.progressbar else None
with self._establish() as ssh, ssh.open_sftp() as sftp:
try:
# If the remote path is a directory, use the original filename.
@@ -210,7 +209,11 @@ class SshC:
except IOError:
path = self.path
finally:
- sftp.put(location, path, callback=callback)
+ if self.progressbar:
+ with Progressbar() as p:
+ sftp.put(location, path, callback=p.progress)
+ else:
+ sftp.put(location, path)
class HttpC:
@@ -264,10 +267,9 @@ class HttpC:
with s.get(final_urlstring, stream=True,
timeout=self.timeout) as r, open(location, 'wb') as f:
if self.progressbar and size:
- progress = make_incremental_progressbar(CHUNK_SIZE / size)
- next(progress)
- for chunk in iter(lambda: begin(next(progress), r.raw.read(CHUNK_SIZE)), b''):
- f.write(chunk)
+ with Progressbar(CHUNK_SIZE / size) as p:
+ for chunk in iter(lambda: begin(p.increment(), r.raw.read(CHUNK_SIZE)), b''):
+ f.write(chunk)
else:
# We'll try to stream the download directly with `copyfileobj()` so that large
# files (like entire VyOS images) don't occupy much memory.
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 3be486cc4..c778d0de8 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -582,10 +582,11 @@ def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'):
def nft_default_rule(fw_conf, fw_name, ipv6=False):
output = ['counter']
default_action = fw_conf['default_action']
+ family = 'ipv6' if ipv6 else 'ipv4'
if 'enable_default_log' in fw_conf:
action_suffix = default_action[:1].upper()
- output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}]"')
+ output.append(f'log prefix "[{family}-{fw_name[:19]}-default-{action_suffix}]"')
#output.append(nft_action(default_action))
output.append(f'{default_action}')
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
index 9484eacdd..d36b6fcfb 100644
--- a/python/vyos/utils/dict.py
+++ b/python/vyos/utils/dict.py
@@ -199,6 +199,31 @@ def dict_search_recursive(dict_object, key, path=[]):
for x in dict_search_recursive(j, key, new_path):
yield x
+
+def dict_set(key_path, value, dict_object):
+ """ Set value to Python dictionary (dict_object) using path to key delimited by dot (.).
+ The key will be added if it does not exist.
+ """
+ path_list = key_path.split(".")
+ dynamic_dict = dict_object
+ if len(path_list) > 0:
+ for i in range(0, len(path_list)-1):
+ dynamic_dict = dynamic_dict[path_list[i]]
+ dynamic_dict[path_list[len(path_list)-1]] = value
+
+def dict_delete(key_path, dict_object):
+ """ Delete key in Python dictionary (dict_object) using path to key delimited by dot (.).
+ """
+ path_dict = dict_object
+ path_list = key_path.split('.')
+ inside = path_list[:-1]
+ if not inside:
+ del dict_object[path_list]
+ else:
+ for key in path_list[:-1]:
+ path_dict = path_dict[key]
+ del path_dict[path_list[len(path_list)-1]]
+
def dict_to_list(d, save_key_to=None):
""" Convert a dict to a list of dicts.
@@ -228,6 +253,39 @@ def dict_to_list(d, save_key_to=None):
return collect
+def dict_to_paths_values(conf: dict) -> dict:
+ """
+ Convert nested dictionary to simple dictionary, where key is a path is delimited by dot (.).
+ """
+ list_of_paths = []
+ dict_of_options ={}
+ for path in dict_to_key_paths(conf):
+ str_path = '.'.join(path)
+ list_of_paths.append(str_path)
+
+ for path in list_of_paths:
+ dict_of_options[path] = dict_search(path,conf)
+
+ return dict_of_options
+def dict_to_key_paths(d: dict) -> list:
+ """ Generator to return list of key paths from dict of list[str]|str
+ """
+ def func(d, path):
+ if isinstance(d, dict):
+ if not d:
+ yield path
+ for k, v in d.items():
+ for r in func(v, path + [k]):
+ yield r
+ elif isinstance(d, list):
+ yield path
+ elif isinstance(d, str):
+ yield path
+ else:
+ raise ValueError('object is not a dict of strings/list of strings')
+ for r in func(d, []):
+ yield r
+
def dict_to_paths(d: dict) -> list:
""" Generator to return list of paths from dict of list[str]|str
"""
@@ -305,3 +363,4 @@ class FixedDict(dict):
if k not in self._allowed:
raise ConfigError(f'Option "{k}" has no defined default')
super().__setitem__(k, v)
+
diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py
index 843494855..5fffa62f8 100644
--- a/python/vyos/utils/io.py
+++ b/python/vyos/utils/io.py
@@ -24,45 +24,6 @@ def print_error(str='', end='\n'):
sys.stderr.write(end)
sys.stderr.flush()
-def make_progressbar():
- """
- Make a procedure that takes two arguments `done` and `total` and prints a
- progressbar based on the ratio thereof, whose length is determined by the
- width of the terminal.
- """
- import shutil, math
- col, _ = shutil.get_terminal_size()
- col = max(col - 15, 20)
- def print_progressbar(done, total):
- if done <= total:
- increment = total / col
- length = math.ceil(done / increment)
- percentage = str(math.ceil(100 * done / total)).rjust(3)
- print_error(f'[{length * "#"}{(col - length) * "_"}] {percentage}%', '\r')
- # Print a newline so that the subsequent prints don't overwrite the full bar.
- if done == total:
- print_error()
- return print_progressbar
-
-def make_incremental_progressbar(increment: float):
- """
- Make a generator that displays a progressbar that grows monotonically with
- every iteration.
- First call displays it at 0% and every subsequent iteration displays it
- at `increment` increments where 0.0 < `increment` < 1.0.
- Intended for FTP and HTTP transfers with stateless callbacks.
- """
- print_progressbar = make_progressbar()
- total = 0.0
- while total < 1.0:
- print_progressbar(total, 1.0)
- yield
- total += increment
- print_progressbar(1, 1)
- # Ignore further calls.
- while True:
- yield
-
def ask_input(question, default='', numeric_only=False, valid_responses=[]):
question_out = question
if default:
diff --git a/scripts/import-conf-mode-commands b/scripts/import-conf-mode-commands
deleted file mode 100755
index 996b31c9c..000000000
--- a/scripts/import-conf-mode-commands
+++ /dev/null
@@ -1,255 +0,0 @@
-#!/usr/bin/env python3
-#
-# build-command-template: converts old style commands definitions to XML
-#
-# Copyright (C) 2019 VyOS maintainers <maintainers@vyos.net>
-#
-# 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, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
-# USA
-
-
-import os
-import re
-import sys
-
-from lxml import etree
-
-
-# Node types
-NODE = 0
-LEAF_NODE = 1
-TAG_NODE = 2
-
-def parse_command_data(t):
- regs = {
- 'help': r'\bhelp:(.*)(?:\n|$)',
- 'priority': r'\bpriority:(.*)(?:\n|$)',
- 'type': r'\btype:(.*)(?:\n|$)',
- 'syntax_expression_var': r'\bsyntax:expression: \$VAR\(\@\) in (.*)'
- }
-
- data = {'multi': False, 'help': ""}
-
- for r in regs:
- try:
- data[r] = re.search(regs[r], t).group(1).strip()
- except:
- data[r] = None
-
- # val_help is special: there can be multiple instances
- val_help_strings = re.findall(r'\bval_help:(.*)(?:\n|$)', t)
- val_help = []
- for v in val_help_strings:
- try:
- fmt, msg = re.match(r'\s*(.*)\s*;\s*(.*)\s*(?:\n|$)', v).groups()
- except:
- fmt = "<text>"
- msg = v
- val_help.append((fmt, msg))
- data['val_help'] = val_help
-
- # multi is on/off
- if re.match(r'\bmulti:', t):
- data['multi'] = True
-
- return(data)
-
-def walk(tree, base_path, name):
- path = os.path.join(base_path, name)
-
- contents = os.listdir(path)
-
- # Determine node type and create XML element for the node
- # Tag node dirs will always have 'node.tag' subdir and 'node.def' file
- # Leaf node dirs have nothing but a 'node.def' file
- # Everything that doesn't match either of these patterns is a normal node
- if 'node.tag' in contents:
- print("Creating a tag node from {0}".format(path))
- elem = etree.Element('tagNode')
- node_type = TAG_NODE
- elif contents == ['node.def']:
- print("Creating a leaf node from {0}".format(path))
- elem = etree.Element('leafNode')
- node_type = LEAF_NODE
- else:
- print("Creating a node from {0}".format(path))
- elem = etree.Element('node')
- node_type = NODE
-
- # Read and parse the command definition data (the 'node.def' file)
- with open(os.path.join(path, 'node.def'), 'r') as f:
- node_def = f.read()
- data = parse_command_data(node_def)
-
- # Import the data into the properties element
- props_elem = etree.Element('properties')
-
- if data['priority']:
- # Priority values sometimes come with comments that explain the value choice
- try:
- prio, prio_comment = re.match(r'\s*(\d+)\s*#(.*)', data['priority']).groups()
- except:
- prio = data['priority'].strip()
- prio_comment = None
- prio_elem = etree.Element('priority')
- prio_elem.text = prio
- props_elem.append(prio_elem)
- if prio_comment:
- prio_comment_elem = etree.Comment(prio_comment)
- props_elem.append(prio_comment_elem)
-
- if data['multi']:
- multi_elem = etree.Element('multi')
- props_elem.append(multi_elem)
-
- if data['help']:
- help_elem = etree.Element('help')
- help_elem.text = data['help']
- props_elem.append(help_elem)
-
- # For leaf nodes, absense of a type: tag means they take no values
- # For any other nodes, it doesn't mean anything
- if not data['type'] and (node_type == LEAF_NODE):
- valueless = etree.Element('valueless')
- props_elem.append(valueless)
-
- # There can be only one constraint element in the definition
- # Create it now, we'll modify it in the next two cases, then append
- constraint_elem = etree.Element('constraint')
- has_constraint = False
-
- # Add regexp field for multiple options
- if data['syntax_expression_var']:
- regex = etree.Element('regex')
- constraint_error=etree.Element('constraintErrorMessage')
- values = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(1)
- message = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(2)
- values = re.findall(r'\"(.+?)\"', values)
- regex.text = '|'.join(values)
- constraint_error.text = re.sub('\".*?VAR.*?\"', '', message)
- constraint_error.text = re.sub(r'[\"|\\]', '', message)
- constraint_elem.append(regex)
- props_elem.append(constraint_elem)
- props_elem.append(constraint_error)
-
- if data['val_help']:
- for vh in data['val_help']:
- vh_elem = etree.Element('valueHelp')
-
- vh_fmt_elem = etree.Element('format')
- # Many commands use special "u32:<start>-<end>" format for ranges
- if re.match(r'u32:', vh[0]):
- vh_fmt = re.match(r'u32:(.*)', vh[0]).group(1).strip()
-
- # If valid range of values is specified in val_help, we can automatically
- # create a constraint for it
- # Extracting it from syntax:expression: would be much more complicated
- vh_validator = etree.Element('validator')
- vh_validator.set("name", "numeric")
- vh_validator.set("argument", "--range {0}".format(vh_fmt))
- constraint_elem.append(vh_validator)
- has_constraint = True
- else:
- vh_fmt = vh[0]
- vh_fmt_elem.text = vh_fmt
-
- vh_help_elem = etree.Element('description')
- vh_help_elem.text = vh[1]
-
- vh_elem.append(vh_fmt_elem)
- vh_elem.append(vh_help_elem)
- props_elem.append(vh_elem)
-
- # Translate the "type:" to the new validator system
- if data['type']:
- t = data['type']
- if t == 'txt':
- # Can't infer anything from the generic "txt" type
- pass
- else:
- validator = etree.Element('validator')
- if t == 'u32':
- validator.set('name', 'numeric')
- validator.set('argument', '--non-negative')
- elif t == 'ipv4':
- validator.set('name', 'ipv4-address')
- elif t == 'ipv4net':
- validator.set('name', 'ipv4-prefix')
- elif t == 'ipv6':
- validator.set('name', 'ipv6-address')
- elif t == 'ipv6net':
- validator.set('name', 'ipv6-prefix')
- elif t == 'macaddr':
- validator.set('name', 'mac-address')
- else:
- print("Warning: unsupported type \'{0}\'".format(t))
- validator = None
-
- if (validator is not None) and (not has_constraint):
- # If has_constraint is true, it means a more specific validator
- # was already extracted from another option
- constraint_elem.append(validator)
- has_constraint = True
-
- if has_constraint:
- props_elem.append(constraint_elem)
-
- elem.append(props_elem)
-
- elem.set("name", name)
-
- if node_type != LEAF_NODE:
- children = etree.Element('children')
-
- # Create the next level dir path,
- # accounting for the "virtual" node.tag subdir for tag nodes
- next_level = path
- if node_type == TAG_NODE:
- next_level = os.path.join(path, 'node.tag')
-
- # Walk the subdirs of the next level
- for d in os.listdir(next_level):
- dp = os.path.join(next_level, d)
- if os.path.isdir(dp):
- walk(children, next_level, d)
-
- elem.append(children)
-
- tree.append(elem)
-
-if __name__ == '__main__':
- if len(sys.argv) < 2:
- print("Usage: {0} <base path>".format(sys.argv[0]))
- sys.exit(1)
- else:
- base_path = sys.argv[1]
-
- root = etree.Element('interfaceDefinition')
- contents = os.listdir(base_path)
- elem = etree.Element('node')
- elem.set('name', os.path.basename(base_path))
- children = etree.Element('children')
-
- for c in contents:
- path = os.path.join(base_path, c)
- if os.path.isdir(path):
- walk(children, base_path, c)
-
- elem.append(children)
- root.append(elem)
-
- xml_data = etree.tostring(root, pretty_print=True).decode()
- with open('output.xml', 'w') as f:
- f.write(xml_data)
diff --git a/smoketest/configs/bgp-evpn-l2vpn-leaf b/smoketest/configs/bgp-evpn-l2vpn-leaf
index 020490186..ab46fbb02 100644
--- a/smoketest/configs/bgp-evpn-l2vpn-leaf
+++ b/smoketest/configs/bgp-evpn-l2vpn-leaf
@@ -33,7 +33,6 @@ interfaces {
parameters {
nolearning
}
- port 4789
source-address 172.29.0.1
vni 100
}
diff --git a/smoketest/configs/cluster-basic b/smoketest/configs/cluster-basic
new file mode 100644
index 000000000..1e34999c1
--- /dev/null
+++ b/smoketest/configs/cluster-basic
@@ -0,0 +1,62 @@
+cluster {
+ dead-interval 500
+ group VyOS {
+ auto-failback true
+ primary vyos1
+ secondary vyos2
+ service 192.0.2.10/24/eth1
+ service 192.0.2.20/24
+ }
+ interface eth1
+ keepalive-interval 100
+ monitor-dead-interval 420
+ pre-shared-secret qwerty
+}
+interfaces {
+ ethernet eth0 {
+ duplex auto
+ smp-affinity auto
+ speed auto
+ }
+ ethernet eth1 {
+ address 192.0.2.1/24
+ duplex auto
+ smp-affinity auto
+ speed auto
+ }
+ loopback lo {
+ }
+}
+system {
+ config-management {
+ commit-revisions 100
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
+ plaintext-password ""
+ }
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+ time-zone Antarctica/South_Pole
+}
+// Warning: Do not remove the following line.
+// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.3.3
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 7b4ba11d0..0de4354d2 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -226,6 +226,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'ttl', 'gt', '102'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'enable-default-log'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'protocol', 'tcp'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'destination', 'port', '22'])
@@ -248,7 +249,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark])
- self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'enable-default-log'])
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'action', 'drop'])
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'protocol', 'gre'])
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'interface-name', interface_inv])
@@ -262,21 +264,24 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
nftables_search = [
['chain VYOS_FORWARD_filter'],
- ['type filter hook forward priority filter; policy drop;'],
+ ['type filter hook forward priority filter; policy accept;'],
['tcp dport 22', 'limit rate 5/minute', 'accept'],
['tcp dport 22', 'add @RECENT_FWD_filter_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'meta pkttype host', 'drop'],
+ ['log prefix "[ipv4-FWD-filter-default-D]"','FWD-filter default-action drop', 'drop'],
['chain VYOS_INPUT_filter'],
['type filter hook input priority filter; policy accept;'],
['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface_wc}"', 'meta pkttype broadcast', 'accept'],
['meta l4proto gre', f'ct mark {mark_hex}', 'return'],
+ ['INP-filter default-action accept', 'accept'],
['chain VYOS_OUTPUT_filter'],
['type filter hook output priority filter; policy accept;'],
['meta l4proto gre', f'oifname != "{interface}"', 'drop'],
['meta l4proto icmp', f'ct mark {mark_hex}', 'return'],
+ ['log prefix "[ipv4-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
['chain NAME_smoketest'],
['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'],
['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'],
- ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop']
+ ['log prefix "[ipv4-smoketest-default-D]"','smoketest default-action', 'drop']
]
self.verify_nftables(nftables_search, 'ip vyos_filter')
@@ -326,16 +331,18 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
nftables_search = [
['chain VYOS_FORWARD_filter'],
- ['type filter hook forward priority filter; policy drop;'],
+ ['type filter hook forward priority filter; policy accept;'],
['ip saddr 198.51.100.1', 'meta mark 0x000003f2', f'jump NAME_{name}'],
+ ['FWD-filter default-action drop', 'drop'],
['chain VYOS_INPUT_filter'],
['type filter hook input priority filter; policy accept;'],
['meta mark != 0x000181cd', 'meta l4proto tcp','queue to 3'],
['meta l4proto udp','queue flags bypass,fanout to 0-15'],
+ ['INP-filter default-action accept', 'accept'],
[f'chain NAME_{name}'],
['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'],
['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'],
- [f'log prefix "[{name}-default-D]"', 'drop']
+ [f'log prefix "[ipv4-{name}-default-D]"', 'drop']
]
self.verify_nftables(nftables_search, 'ip vyos_filter')
@@ -411,12 +418,14 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log-options', 'level', 'crit'])
self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'enable-default-log'])
self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'action', 'reject'])
self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'protocol', 'tcp_udp'])
self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'destination', 'port', '8888'])
self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'inbound-interface', 'interface-name', interface])
self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'enable-default-log'])
self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'action', 'return'])
self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'protocol', 'gre'])
self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'outbound-interface', 'interface-name', interface])
@@ -432,15 +441,18 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
['chain VYOS_IPV6_FORWARD_filter'],
['type filter hook forward priority filter; policy accept;'],
['meta l4proto { tcp, udp }', 'th dport 8888', f'iifname "{interface}"', 'reject'],
+ ['log prefix "[ipv6-FWD-filter-default-A]"','FWD-filter default-action accept', 'accept'],
['chain VYOS_IPV6_INPUT_filter'],
['type filter hook input priority filter; policy accept;'],
['meta l4proto udp', 'ip6 saddr 2002::1:2', f'iifname "{interface}"', 'accept'],
+ ['INP-filter default-action accept', 'accept'],
['chain VYOS_IPV6_OUTPUT_filter'],
- ['type filter hook output priority filter; policy drop;'],
+ ['type filter hook output priority filter; policy accept;'],
['meta l4proto gre', f'oifname "{interface}"', 'return'],
+ ['log prefix "[ipv6-OUT-filter-default-D]"','OUT-filter default-action drop', 'drop'],
[f'chain NAME6_{name}'],
['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'],
- [f'"{name} default-action drop"', f'log prefix "[{name}-default-D]"', 'drop']
+ [f'"{name} default-action drop"', f'log prefix "[ipv6-{name}-default-D]"', 'drop']
]
self.verify_nftables(nftables_search, 'ip6 vyos_filter')
@@ -483,7 +495,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
['ip6 saddr 2001:db8::/64', 'meta mark != 0x000019ff-0x00001e56', f'jump NAME6_{name}'],
[f'chain NAME6_{name}'],
['ip6 length { 65, 513, 1025 }', 'ip6 dscp { af21, 0x35 }', 'accept'],
- [f'log prefix "[{name}-default-D]"', 'drop']
+ [f'log prefix "[ipv6-{name}-default-D]"', 'drop']
]
self.verify_nftables(nftables_search, 'ip6 vyos_filter')
@@ -635,6 +647,44 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
with open(path, 'r') as f:
self.assertNotEqual(f.read().strip(), conf['default'], msg=path)
+### Zone
+ def test_zone_basic(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
+ self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone'])
+ self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VYOS_ZONE_FORWARD'],
+ ['type filter hook forward priority filter + 1'],
+ ['chain VYOS_ZONE_OUTPUT'],
+ ['type filter hook output priority filter + 1'],
+ ['chain VYOS_ZONE_LOCAL'],
+ ['type filter hook input priority filter + 1'],
+ ['chain VZONE_smoketest-eth0'],
+ ['chain VZONE_smoketest-local_IN'],
+ ['chain VZONE_smoketest-local_OUT'],
+ ['oifname "eth0"', 'jump VZONE_smoketest-eth0'],
+ ['jump VZONE_smoketest-local_IN'],
+ ['jump VZONE_smoketest-local_OUT'],
+ ['iifname "eth0"', 'jump NAME_smoketest'],
+ ['oifname "eth0"', 'jump NAME_smoketest']
+ ]
+
+ nftables_output = cmd('sudo nft list table ip vyos_filter')
+
+ for search in nftables_search:
+ matched = False
+ for line in nftables_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(matched)
+
+
def test_flow_offload(self):
self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0'])
self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'hardware'])
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 4a7e2418c..66c348976 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -506,11 +506,13 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = 'vtun5001'
path = base_path + [interface]
+ encryption_cipher = 'aes256'
self.cli_set(path + ['mode', 'site-to-site'])
self.cli_set(path + ['local-address', '10.0.0.2'])
self.cli_set(path + ['remote-address', '192.168.0.3'])
self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
self.cli_commit()
@@ -548,6 +550,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
port = ''
local_address = ''
remote_address = ''
+ encryption_cipher = 'aes256'
for ii in num_range:
interface = f'vtun{ii}'
@@ -571,6 +574,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['remote-port', port])
self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
self.cli_set(path + ['remote-address', remote_address])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
self.cli_set(path + ['vrf', vrf_name])
self.cli_commit()
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
index 4ac422d5f..51a33f978 100755
--- a/smoketest/scripts/cli/test_policy.py
+++ b/smoketest/scripts/cli/test_policy.py
@@ -1541,6 +1541,56 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
self.assertEqual(sort_ip(tmp), sort_ip(original))
+ # Test set table for destination, source, protocol, fwmark and port
+ def test_protocol_port_address_fwmark_table_id(self):
+ path = base_path + ['local-route']
+
+ dst = '203.0.113.5'
+ src_list = ['203.0.113.1', '203.0.113.2']
+ rule = '23'
+ fwmark = '123456'
+ table = '123'
+ new_table = '111'
+ proto = 'udp'
+ new_proto = 'tcp'
+ src_port = '5555'
+ dst_port = '8888'
+
+ self.cli_set(path + ['rule', rule, 'set', 'table', table])
+ self.cli_set(path + ['rule', rule, 'destination', 'address', dst])
+ self.cli_set(path + ['rule', rule, 'source', 'port', src_port])
+ self.cli_set(path + ['rule', rule, 'protocol', proto])
+ self.cli_set(path + ['rule', rule, 'fwmark', fwmark])
+ self.cli_set(path + ['rule', rule, 'destination', 'port', dst_port])
+ for src in src_list:
+ self.cli_set(path + ['rule', rule, 'source', 'address', src])
+
+ self.cli_commit()
+
+ original = """
+ 23: from 203.0.113.1 to 203.0.113.5 fwmark 0x1e240 ipproto udp sport 5555 dport 8888 lookup 123
+ 23: from 203.0.113.2 to 203.0.113.5 fwmark 0x1e240 ipproto udp sport 5555 dport 8888 lookup 123
+ """
+ tmp = cmd(f'ip rule show prio {rule}')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
+ # Change table and protocol, delete fwmark and source port
+ self.cli_delete(path + ['rule', rule, 'fwmark'])
+ self.cli_delete(path + ['rule', rule, 'source', 'port'])
+ self.cli_set(path + ['rule', rule, 'set', 'table', new_table])
+ self.cli_set(path + ['rule', rule, 'protocol', new_proto])
+
+ self.cli_commit()
+
+ original = """
+ 23: from 203.0.113.1 to 203.0.113.5 ipproto tcp dport 8888 lookup 111
+ 23: from 203.0.113.2 to 203.0.113.5 ipproto tcp dport 8888 lookup 111
+ """
+ tmp = cmd(f'ip rule show prio {rule}')
+
+ self.assertEqual(sort_ip(tmp), sort_ip(original))
+
# Test set table for sources with fwmark
def test_fwmark_sources_table_id(self):
path = base_path + ['local-route']
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 967958cab..5e3402fa8 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -340,6 +340,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
# AFI maximum path support
self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ebgp', max_path_v4])
self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp])
+ self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ebgp', max_path_v4])
+ self.cli_set(base_path + ['address-family', 'ipv4-labeled-unicast', 'maximum-paths', 'ibgp', max_path_v4ibgp])
self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ebgp', max_path_v6])
self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'maximum-paths', 'ibgp', max_path_v6ibgp])
@@ -373,6 +375,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config)
self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config)
+ afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast')
+ self.assertIn(f' maximum-paths {max_path_v4}', afiv4_config)
+ self.assertIn(f' maximum-paths ibgp {max_path_v4ibgp}', afiv4_config)
+
afiv6_config = self.getFRRconfig(' address-family ipv6 unicast')
self.assertIn(f' maximum-paths {max_path_v6}', afiv6_config)
self.assertIn(f' maximum-paths ibgp {max_path_v6ibgp}', afiv6_config)
diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py
index 3743be788..81e7326f8 100755
--- a/smoketest/scripts/cli/test_qos.py
+++ b/smoketest/scripts/cli/test_qos.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-2023 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
@@ -543,5 +543,60 @@ class TestQoS(VyOSUnitTestSHIM.TestCase):
dport = int(match_config['dport'])
self.assertEqual(f'{dport:x}', filter['options']['match']['value'])
+
+ def test_11_shaper(self):
+ bandwidth = 250
+ default_bandwidth = 20
+ default_ceil = 30
+ class_bandwidth = 50
+ class_ceil = 80
+ dst_address = '192.0.2.8/32'
+
+ for interface in self._interfaces:
+ shaper_name = f'qos-shaper-{interface}'
+
+ self.cli_set(base_path + ['interface', interface, 'egress', shaper_name])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'bandwidth', f'{bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'bandwidth', f'{default_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'ceiling', f'{default_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'default', 'queue-type', 'fair-queue'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'bandwidth', f'{class_bandwidth}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'ceiling', f'{class_ceil}mbit'])
+ self.cli_set(base_path + ['policy', 'shaper', shaper_name, 'class', '23', 'match', '10', 'ip', 'destination', 'address', dst_address])
+
+ bandwidth += 1
+ default_bandwidth += 1
+ default_ceil += 1
+ class_bandwidth += 1
+ class_ceil += 1
+
+ # commit changes
+ self.cli_commit()
+
+ bandwidth = 250
+ default_bandwidth = 20
+ default_ceil = 30
+ class_bandwidth = 50
+ class_ceil = 80
+
+ for interface in self._interfaces:
+ config_entries = (
+ f'root rate {bandwidth}Mbit ceil {bandwidth}Mbit',
+ f'prio 0 rate {class_bandwidth}Mbit ceil {class_ceil}Mbit',
+ f'prio 7 rate {default_bandwidth}Mbit ceil {default_ceil}Mbit'
+ )
+
+ output = cmd(f'tc class show dev {interface}')
+
+ for config_entry in config_entries:
+ self.assertIn(config_entry, output)
+
+ bandwidth += 1
+ default_bandwidth += 1
+ default_ceil += 1
+ class_bandwidth += 1
+ class_ceil += 1
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index f6480ab0a..9791cf009 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -374,12 +374,82 @@ def verify(firewall):
for rule_id, rule_conf in name_conf['rule'].items():
verify_rule(firewall, rule_conf, True)
+ #### ZONESSSS
+ local_zone = False
+ zone_interfaces = []
+
+ if 'zone' in firewall:
+ for zone, zone_conf in firewall['zone'].items():
+ if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
+ raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
+
+ if 'local_zone' in zone_conf:
+ if local_zone:
+ raise ConfigError('There cannot be multiple local zones')
+ if 'interface' in zone_conf:
+ raise ConfigError('Local zone cannot have interfaces assigned')
+ if 'intra_zone_filtering' in zone_conf:
+ raise ConfigError('Local zone cannot use intra-zone-filtering')
+ local_zone = True
+
+ if 'interface' in zone_conf:
+ found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces]
+
+ if found_duplicates:
+ raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
+
+ zone_interfaces += zone_conf['interface']
+
+ if 'intra_zone_filtering' in zone_conf:
+ intra_zone = zone_conf['intra_zone_filtering']
+
+ if len(intra_zone) > 1:
+ raise ConfigError('Only one intra-zone-filtering action must be specified')
+
+ if 'firewall' in intra_zone:
+ v4_name = dict_search_args(intra_zone, 'firewall', 'name')
+ if v4_name and not dict_search_args(firewall, 'ipv4', 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name')
+ if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+ if not v4_name and not v6_name:
+ raise ConfigError('No firewall names specified for intra-zone-filtering')
+
+ if 'from' in zone_conf:
+ for from_zone, from_conf in zone_conf['from'].items():
+ if from_zone not in firewall['zone']:
+ raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
+
+ v4_name = dict_search_args(from_conf, 'firewall', 'name')
+ if v4_name and not dict_search_args(firewall, 'ipv4', 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
+ if v6_name and not dict_search_args(firewall, 'ipv6', 'name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
return None
def generate(firewall):
if not os.path.exists(nftables_conf):
firewall['first_install'] = True
+ if 'zone' in firewall:
+ for local_zone, local_zone_conf in firewall['zone'].items():
+ if 'local_zone' not in local_zone_conf:
+ continue
+
+ local_zone_conf['from_local'] = {}
+
+ for zone, zone_conf in firewall['zone'].items():
+ if zone == local_zone or 'from' not in zone_conf:
+ continue
+ if local_zone in zone_conf['from']:
+ local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
+
render(nftables_conf, 'firewall/nftables.j2', firewall)
return None
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 81ee39df1..206f513c8 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -28,6 +28,7 @@ from vyos.ifconfig import Section
from vyos.template import render
from vyos.utils.process import call
from vyos.utils.process import cmd
+from vyos.utils.process import run
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
@@ -116,6 +117,30 @@ def _nftables_config(configured_ifaces, direction, length=None):
cmd(command, raising=ConfigError)
+def _nftables_trigger_setup(operation: str) -> None:
+ """Add a dummy rule to unlock the main pmacct loop with a packet-trigger
+
+ Args:
+ operation (str): 'add' or 'delete' a trigger
+ """
+ # check if a chain exists
+ table_exists = False
+ if run('nft -snj list table ip pmacct') == 0:
+ table_exists = True
+
+ if operation == 'delete' and table_exists:
+ nft_cmd: str = 'nft delete table ip pmacct'
+ cmd(nft_cmd, raising=ConfigError)
+ if operation == 'add' and not table_exists:
+ nft_cmds: list[str] = [
+ 'nft add table ip pmacct',
+ 'nft add chain ip pmacct pmacct_out { type filter hook output priority raw - 50 \\; policy accept \\; }',
+ 'nft add rule ip pmacct pmacct_out oif lo ip daddr 127.0.254.0 counter log group 2 snaplen 1 queue-threshold 0 comment NFLOG_TRIGGER'
+ ]
+ for nft_cmd in nft_cmds:
+ cmd(nft_cmd, raising=ConfigError)
+
+
def get_config(config=None):
if config:
conf = config
@@ -252,7 +277,6 @@ def generate(flow_config):
call('systemctl daemon-reload')
def apply(flow_config):
- action = 'restart'
# Check if flow-accounting was removed and define command
if not flow_config:
_nftables_config([], 'ingress')
@@ -262,6 +286,10 @@ def apply(flow_config):
call(f'systemctl stop {systemd_service}')
if os.path.exists(uacctd_conf_path):
os.unlink(uacctd_conf_path)
+
+ # must be done after systemctl
+ _nftables_trigger_setup('delete')
+
return
# Start/reload flow-accounting daemon
@@ -277,6 +305,10 @@ def apply(flow_config):
else:
_nftables_config([], 'egress')
+ # add a trigger for signal processing
+ _nftables_trigger_setup('add')
+
+
if __name__ == '__main__':
try:
config = get_config()
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 793a90d88..d8fe3b736 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -27,6 +27,7 @@ from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
from vyos.template import render
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -130,7 +131,10 @@ def apply(http_api):
service_name = 'vyos-http-api.service'
if http_api is not None:
- call(f'systemctl restart {service_name}')
+ if is_systemd_service_running(f'{service_name}'):
+ call(f'systemctl reload {service_name}')
+ else:
+ call(f'systemctl restart {service_name}')
else:
call(f'systemctl stop {service_name}')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 0bd306ed0..1179e3e4f 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -18,7 +18,6 @@ import os
from sys import exit
from netifaces import interfaces
-
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
@@ -34,10 +33,13 @@ from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import BondIf
+from vyos.ifconfig.ethernet import EthernetIf
from vyos.ifconfig import Section
from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
from vyos.configdict import has_address_configured
from vyos.configdict import has_vrf_configured
+from vyos.configdep import set_dependents, call_dependents
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -90,7 +92,6 @@ def get_config(config=None):
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
-
# Reset config level to interfaces
old_level = conf.get_level()
conf.set_level(['interfaces'])
@@ -102,6 +103,10 @@ def get_config(config=None):
tmp = {}
for interface in interfaces_removed:
+ # if member is deleted from bond, add dependencies to call
+ # ethernet commit again in apply function
+ # to apply options under ethernet section
+ set_dependents('ethernet', conf, interface)
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
tmp[interface] = {'disable': ''}
@@ -116,9 +121,21 @@ def get_config(config=None):
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
+
+ interface_ethernet_config = conf.get_config_dict(
+ ['interfaces', 'ethernet', interface],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=False,
+ with_recursive_defaults=False)
+
+ interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+
# Check if member interface is a new member
if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
+ interface_config['new_added'] = {}
# Check if member interface is disabled
conf.set_level(['interfaces'])
@@ -151,7 +168,6 @@ def get_config(config=None):
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
if tmp: interface_config['has_vrf'] = {}
-
return bond
@@ -212,6 +228,14 @@ def verify(bond):
if 'has_vrf' in interface_config:
raise ConfigError(error_msg + 'it has a VRF assigned!')
+ if 'new_added' in interface_config and 'config_paths' in interface_config:
+ for option_path, option_value in interface_config['config_paths'].items():
+ if option_path in EthernetIf.get_bond_member_allowed_options() :
+ continue
+ if option_path in BondIf.get_inherit_bond_options():
+ continue
+ raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!')
+
if 'primary' in bond:
if bond['primary'] not in bond['member']['interface']:
raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
@@ -227,13 +251,17 @@ def generate(bond):
def apply(bond):
b = BondIf(bond['ifname'])
-
if 'deleted' in bond:
# delete interface
b.remove()
else:
b.update(bond)
-
+ if dict_search('member.interface_remove', bond):
+ try:
+ call_dependents()
+ except ConfigError:
+ raise ConfigError('Error in updating ethernet interface '
+ 'after deleting it from bond')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index f3e65ad5e..7374a29f7 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import pprint
from glob import glob
from sys import exit
@@ -35,6 +36,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_bond_bridge_member
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import BondIf
from vyos.pki import find_chain
from vyos.pki import encode_certificate
from vyos.pki import load_certificate
@@ -42,6 +44,9 @@ from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.utils.process import call
from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
+from vyos.utils.dict import dict_set
+from vyos.utils.dict import dict_delete
from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
@@ -51,6 +56,90 @@ airbag.enable()
cfg_dir = '/run/wpa_supplicant'
wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
+def update_bond_options(conf: Config, eth_conf: dict) -> list:
+ """
+ Return list of blocked options if interface is a bond member
+ :param conf: Config object
+ :type conf: Config
+ :param eth_conf: Ethernet config dictionary
+ :type eth_conf: dict
+ :return: List of blocked options
+ :rtype: list
+ """
+ blocked_list = []
+ bond_name = list(eth_conf['is_bond_member'].keys())[0]
+ config_without_defaults = conf.get_config_dict(
+ ['interfaces', 'ethernet', eth_conf['ifname']],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=False,
+ with_recursive_defaults=False)
+ config_with_defaults = conf.get_config_dict(
+ ['interfaces', 'ethernet', eth_conf['ifname']],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True)
+ bond_config_with_defaults = conf.get_config_dict(
+ ['interfaces', 'bonding', bond_name],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True)
+ eth_dict_paths = dict_to_paths_values(config_without_defaults)
+ eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']]
+
+ #if option is configured under ethernet section
+ for option_path, option_value in eth_dict_paths.items():
+ bond_option_value = dict_search(option_path, bond_config_with_defaults)
+
+ #If option is allowed for changing then continue
+ if option_path in EthernetIf.get_bond_member_allowed_options():
+ continue
+ # if option is inherited from bond then set valued from bond interface
+ if option_path in BondIf.get_inherit_bond_options():
+ # If option equals to bond option then do nothing
+ if option_value == bond_option_value:
+ continue
+ else:
+ # if ethernet has option and bond interface has
+ # then copy it from bond
+ if bond_option_value is not None:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+ f' Option is inherited from bond "{bond_name}"')
+ dict_set(option_path, bond_option_value, eth_conf)
+ continue
+ # if ethernet has option and bond interface does not have
+ # then delete it form dict and do not apply it
+ else:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+ f' Option is inherited from bond "{bond_name}"')
+ dict_delete(option_path, eth_conf)
+ blocked_list.append(option_path)
+
+ # if inherited option is not configured under ethernet section but configured under bond section
+ for option_path in BondIf.get_inherit_bond_options():
+ bond_option_value = dict_search(option_path, bond_config_with_defaults)
+ if bond_option_value is not None:
+ if option_path not in eth_dict_paths:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member. ' \
+ f'Option is inherited from bond "{bond_name}"')
+ dict_set(option_path, bond_option_value, eth_conf)
+ eth_conf['bond_blocked_changes'] = blocked_list
+ return None
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -68,6 +157,8 @@ def get_config(config=None):
base = ['interfaces', 'ethernet']
ifname, ethernet = get_interface_dict(conf, base)
+ if 'is_bond_member' in ethernet:
+ update_bond_options(conf, ethernet)
if 'deleted' not in ethernet:
if pki: ethernet['pki'] = pki
@@ -80,26 +171,20 @@ def get_config(config=None):
return ethernet
-def verify(ethernet):
- if 'deleted' in ethernet:
- return None
- ifname = ethernet['ifname']
- verify_interface_exists(ifname)
- verify_mtu(ethernet)
- verify_mtu_ipv6(ethernet)
- verify_dhcpv6(ethernet)
- verify_address(ethernet)
- verify_vrf(ethernet)
- verify_bond_bridge_member(ethernet)
- verify_eapol(ethernet)
- verify_mirror_redirect(ethernet)
- ethtool = Ethtool(ifname)
- # No need to check speed and duplex keys as both have default values.
+def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify speed and duplex
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError(
+ 'Speed/Duplex missmatch. Must be both auto or manually configured')
if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
# We need to verify if the requested speed and duplex setting is
@@ -107,37 +192,66 @@ def verify(ethernet):
speed = ethernet['speed']
duplex = ethernet['duplex']
if not ethtool.check_speed_duplex(speed, duplex):
- raise ConfigError(f'Adapter does not support changing speed and duplex '\
- f'settings to: {speed}/{duplex}!')
+ raise ConfigError(
+ f'Adapter does not support changing speed ' \
+ f'and duplex settings to: {speed}/{duplex}!')
+
+def verify_flow_control(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify flow control
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if 'disable_flow_control' in ethernet:
if not ethtool.check_flow_control():
- raise ConfigError('Adapter does not support changing flow-control settings!')
+ raise ConfigError(
+ 'Adapter does not support changing flow-control settings!')
+
+def verify_ring_buffer(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify ring buffer
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if 'ring_buffer' in ethernet:
max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
- raise ConfigError('Driver does not support RX ring-buffer configuration!')
+ raise ConfigError(
+ 'Driver does not support RX ring-buffer configuration!')
max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
- raise ConfigError('Driver does not support TX ring-buffer configuration!')
+ raise ConfigError(
+ 'Driver does not support TX ring-buffer configuration!')
rx = dict_search('ring_buffer.rx', ethernet)
if rx and int(rx) > int(max_rx):
- raise ConfigError(f'Driver only supports a maximum RX ring-buffer '\
+ raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \
f'size of "{max_rx}" bytes!')
tx = dict_search('ring_buffer.tx', ethernet)
if tx and int(tx) > int(max_tx):
- raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
+ raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \
f'size of "{max_tx}" bytes!')
- # verify offloading capabilities
+
+def verify_offload(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify offloading capabilities
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'):
raise ConfigError('Interface does not suport RPS!')
-
driver = ethtool.get_driver_name()
# T3342 - Xen driver requires special treatment
if driver == 'vif':
@@ -145,14 +259,73 @@ def verify(ethernet):
raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
'for MTU size larger then 1500 bytes')
- if {'is_bond_member', 'mac'} <= set(ethernet):
- Warning(f'changing mac address "{mac}" will be ignored as "{ifname}" ' \
- f'is a member of bond "{is_bond_member}"'.format(**ethernet))
+def verify_allowedbond_changes(ethernet: dict):
+ """
+ Verify changed options if interface is in bonding
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ if 'bond_blocked_changes' in ethernet:
+ for option in ethernet['bond_blocked_changes']:
+ raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \
+ f' on interface "{ethernet["ifname"]}".' \
+ f' Interface is a bond member')
+
+
+def verify(ethernet):
+ if 'deleted' in ethernet:
+ return None
+ if 'is_bond_member' in ethernet:
+ verify_bond_member(ethernet)
+ else:
+ verify_ethernet(ethernet)
+
+
+def verify_bond_member(ethernet):
+ """
+ Verification function for ethernet interface which is in bonding
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ ifname = ethernet['ifname']
+ verify_interface_exists(ifname)
+ verify_eapol(ethernet)
+ verify_mirror_redirect(ethernet)
+ ethtool = Ethtool(ifname)
+ verify_speed_duplex(ethernet, ethtool)
+ verify_flow_control(ethernet, ethtool)
+ verify_ring_buffer(ethernet, ethtool)
+ verify_offload(ethernet, ethtool)
+ verify_allowedbond_changes(ethernet)
+
+def verify_ethernet(ethernet):
+ """
+ Verification function for simple ethernet interface
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ ifname = ethernet['ifname']
+ verify_interface_exists(ifname)
+ verify_mtu(ethernet)
+ verify_mtu_ipv6(ethernet)
+ verify_dhcpv6(ethernet)
+ verify_address(ethernet)
+ verify_vrf(ethernet)
+ verify_bond_bridge_member(ethernet)
+ verify_eapol(ethernet)
+ verify_mirror_redirect(ethernet)
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ verify_speed_duplex(ethernet, ethtool)
+ verify_flow_control(ethernet, ethtool)
+ verify_ring_buffer(ethernet, ethtool)
+ verify_offload(ethernet, ethtool)
# use common function to verify VLAN configuration
verify_vlan_config(ethernet)
return None
+
def generate(ethernet):
# render real configuration file once
wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
@@ -192,7 +365,8 @@ def generate(ethernet):
pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
- ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain))
+ ca_chains.append(
+ '\n'.join(encode_certificate(c) for c in ca_full_chain))
write_file(ca_cert_file_path, '\n'.join(ca_chains))
@@ -219,6 +393,7 @@ if __name__ == '__main__':
c = get_config()
verify(c)
generate(c)
+
apply(c)
except ConfigError as e:
print(e)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 05f68112a..ff8144e74 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -168,6 +168,13 @@ def verify(vxlan):
verify_address(vxlan)
verify_bond_bridge_member(vxlan)
verify_mirror_redirect(vxlan)
+
+ # We use a defaultValue for port, thus it's always safe to use
+ if vxlan['port'] == '8472':
+ Warning('Starting from VyOS 1.4, the default port for VXLAN '\
+ 'has been changed to 4789. This matches the IANA assigned '\
+ 'standard port number!')
+
return None
def generate(vxlan):
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 2e8aabb80..91e4fce2c 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -52,19 +52,28 @@ def get_config(config=None):
if tmp:
for rule in (tmp or []):
src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
+ src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
+ dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
+ table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
rule_def = {}
if src:
rule_def = dict_merge({'source': {'address': src}}, rule_def)
+ if src_port:
+ rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
if fwmk:
rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
if iif:
rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
if dst:
rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
+ if dst_port:
+ rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
+ if table:
+ rule_def = dict_merge({'table' : table}, rule_def)
if proto:
rule_def = dict_merge({'protocol' : proto}, rule_def)
dict = dict_merge({dict_id : {rule : rule_def}}, dict)
@@ -79,9 +88,12 @@ def get_config(config=None):
if 'rule' in pbr[route]:
for rule, rule_config in pbr[route]['rule'].items():
src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
+ src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
+ dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
+ table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
# keep track of changes in configuration
# otherwise we might remove an existing node although nothing else has changed
@@ -105,14 +117,32 @@ def get_config(config=None):
if len(src) > 0:
rule_def = dict_merge({'source': {'address': src}}, rule_def)
+ # source port
+ if src_port is None:
+ if 'source' in rule_config:
+ if 'port' in rule_config['source']:
+ tmp = rule_config['source']['port']
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ rule_def = dict_merge({'source': {'port': tmp}}, rule_def)
+ else:
+ changed = True
+ if len(src_port) > 0:
+ rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
+
+ # fwmark
if fwmk is None:
if 'fwmark' in rule_config:
- rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def)
+ tmp = rule_config['fwmark']
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ rule_def = dict_merge({'fwmark': tmp}, rule_def)
else:
changed = True
if len(fwmk) > 0:
rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+ # inbound-interface
if iif is None:
if 'inbound_interface' in rule_config:
rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
@@ -121,6 +151,7 @@ def get_config(config=None):
if len(iif) > 0:
rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+ # destination address
if dst is None:
if 'destination' in rule_config:
if 'address' in rule_config['destination']:
@@ -130,9 +161,35 @@ def get_config(config=None):
if len(dst) > 0:
rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
+ # destination port
+ if dst_port is None:
+ if 'destination' in rule_config:
+ if 'port' in rule_config['destination']:
+ tmp = rule_config['destination']['port']
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ rule_def = dict_merge({'destination': {'port': tmp}}, rule_def)
+ else:
+ changed = True
+ if len(dst_port) > 0:
+ rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
+
+ # table
+ if table is None:
+ if 'set' in rule_config and 'table' in rule_config['set']:
+ rule_def = dict_merge({'table': [rule_config['set']['table']]}, rule_def)
+ else:
+ changed = True
+ if len(table) > 0:
+ rule_def = dict_merge({'table' : table}, rule_def)
+
+ # protocol
if proto is None:
if 'protocol' in rule_config:
- rule_def = dict_merge({'protocol': rule_config['protocol']}, rule_def)
+ tmp = rule_config['protocol']
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ rule_def = dict_merge({'protocol': tmp}, rule_def)
else:
changed = True
if len(proto) > 0:
@@ -192,19 +249,27 @@ def apply(pbr):
for rule, rule_config in pbr[rule_rm].items():
source = rule_config.get('source', {}).get('address', [''])
+ source_port = rule_config.get('source', {}).get('port', [''])
destination = rule_config.get('destination', {}).get('address', [''])
+ destination_port = rule_config.get('destination', {}).get('port', [''])
fwmark = rule_config.get('fwmark', [''])
inbound_interface = rule_config.get('inbound_interface', [''])
protocol = rule_config.get('protocol', [''])
+ table = rule_config.get('table', [''])
- for src, dst, fwmk, iif, proto in product(source, destination, fwmark, inbound_interface, protocol):
+ for src, dst, src_port, dst_port, fwmk, iif, proto, table in product(
+ source, destination, source_port, destination_port,
+ fwmark, inbound_interface, protocol, table):
f_src = '' if src == '' else f' from {src} '
+ f_src_port = '' if src_port == '' else f' sport {src_port} '
f_dst = '' if dst == '' else f' to {dst} '
+ f_dst_port = '' if dst_port == '' else f' dport {dst_port} '
f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
f_iif = '' if iif == '' else f' iif {iif} '
f_proto = '' if proto == '' else f' ipproto {proto} '
+ f_table = '' if table == '' else f' lookup {table} '
- call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}')
+ call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif}{f_table}')
# Generate new config
for route in ['local_route', 'local_route6']:
@@ -218,7 +283,9 @@ def apply(pbr):
for rule, rule_config in pbr_route['rule'].items():
table = rule_config['set'].get('table', '')
source = rule_config.get('source', {}).get('address', ['all'])
+ source_port = rule_config.get('source', {}).get('port', '')
destination = rule_config.get('destination', {}).get('address', ['all'])
+ destination_port = rule_config.get('destination', {}).get('port', '')
fwmark = rule_config.get('fwmark', '')
inbound_interface = rule_config.get('inbound_interface', '')
protocol = rule_config.get('protocol', '')
@@ -227,11 +294,13 @@ def apply(pbr):
f_src = f' from {src} ' if src else ''
for dst in destination:
f_dst = f' to {dst} ' if dst else ''
+ f_src_port = f' sport {source_port} ' if source_port else ''
+ f_dst_port = f' dport {destination_port} ' if destination_port else ''
f_fwmk = f' fwmark {fwmark} ' if fwmark else ''
f_iif = f' iif {inbound_interface} ' if inbound_interface else ''
f_proto = f' ipproto {protocol} ' if protocol else ''
- call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_fwmk}{f_iif} lookup {table}')
+ call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table}')
return None
diff --git a/src/migration-scripts/cluster/1-to-2 b/src/migration-scripts/cluster/1-to-2
new file mode 100755
index 000000000..a2e589155
--- /dev/null
+++ b/src/migration-scripts/cluster/1-to-2
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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 re
+import sys
+
+from vyos.configtree import ConfigTree
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+ file_name = sys.argv[1]
+
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+
+ config = ConfigTree(config_file)
+
+ if not config.exists(['cluster']):
+ # Cluster is not set -- nothing to do at all
+ sys.exit(0)
+
+ # If at least one cluster group is defined, we have real work to do.
+ # If there are no groups, we remove the top-level cluster node at the end of this script anyway.
+ if config.exists(['cluster', 'group']):
+ # First, gather timer and interface settings to duplicate them in all groups,
+ # since in the old cluster they are global, but in VRRP they are always per-group
+
+ global_interface = None
+ if config.exists(['cluster', 'interface']):
+ global_interface = config.return_value(['cluster', 'interface'])
+ else:
+ # Such configs shouldn't exist in practice because interface is a required option.
+ # But since it's possible to specify interface inside 'service' options,
+ # we may be able to convert such configs nonetheless.
+ print("Warning: incorrect cluster config: interface is not defined.", file=sys.stderr)
+
+ # There are three timers: advertise-interval, dead-interval, and monitor-dead-interval
+ # Only the first one makes sense for the VRRP, we translate it to advertise-interval
+ advertise_interval = None
+ if config.exists(['cluster', 'keepalive-interval']):
+ advertise_interval = config.return_value(['cluster', 'keepalive-interval'])
+
+ if advertise_interval is not None:
+ # Cluster had all timers in milliseconds, so we need to convert them to seconds
+ # And ensure they are not shorter than one second
+ advertise_interval = int(advertise_interval) // 1000
+ if advertise_interval < 1:
+ advertise_interval = 1
+
+ # Cluster had password as a global option, in VRRP it's per-group
+ password = None
+ if config.exists(['cluster', 'pre-shared-secret']):
+ password = config.return_value(['cluster', 'pre-shared-secret'])
+
+ # Set up the stage for converting cluster groups to VRRP groups
+ free_vrids = set(range(1,255))
+ vrrp_base_path = ['high-availability', 'vrrp', 'group']
+ if not config.exists(vrrp_base_path):
+ # If VRRP is not set up, create a node and set it to 'tag node'
+ # Setting it to 'tag' is not mandatory but it's better to be consistent
+ # with configs produced by 'save'
+ config.set(vrrp_base_path)
+ config.set_tag(vrrp_base_path)
+ else:
+ # If there are VRRP groups already, we need to find the set of unused VRID numbers to avoid conflicts
+ existing_vrids = set()
+ for vg in config.list_nodes(vrrp_base_path):
+ existing_vrids.add(int(config.return_value(vrrp_base_path + [vg, 'vrid'])))
+ free_vrids = free_vrids.difference(existing_vrids)
+
+ # Now handle cluster groups
+ groups = config.list_nodes(['cluster', 'group'])
+ for g in groups:
+ base_path = ['cluster', 'group', g]
+ service_names = config.return_values(base_path + ['service'])
+
+ # Cluster used to allow services other than IP addresses, at least nominally
+ # Whether that ever worked is a big question, but we need to consider that,
+ # since configs with custom services are definitely impossible to meaningfully migrate now
+ services = {"ip": [], "other": []}
+ for s in service_names:
+ if re.match(r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2})(/[a-z]+\d+)?$', s):
+ services["ip"].append(s)
+ else:
+ services["other"].append(s)
+
+ if services["other"]:
+ print("Cluster config includes non-IP address services and cannot be migrated", file=sys.stderr)
+ sys.exit(1)
+
+ # Cluster allowed virtual IPs for different interfaces within a single group.
+ # VRRP groups are by definition bound to interfaces, so we cannot migrate such configurations.
+ # Thus we need to find out if all addresses either leave the interface unspecified
+ # (in that case the global 'cluster interface' option is used),
+ # or have the same interface, or have the same interface as the global 'cluster interface'.
+
+ # First, we collect all addresses and check if they have interface specified
+ # If not, we substitute the global interface option
+ # or throw an error if it's not in the config.
+ ips = []
+ for ip in services["ip"]:
+ ip_with_intf = re.match(r'^(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})/(?P<intf>[a-z]+\d+)$', ip)
+ if ip_with_intf:
+ ips.append({"ip": ip_with_intf.group("ip"), "interface": ip_with_intf.group("intf")})
+ else:
+ if global_interface is not None:
+ ips.append({"ip": ip, "interface": global_interface})
+ else:
+ print("Error: cluster has groups with IPs without interfaces and 'cluster interface' is not specified.", file=sys.stderr)
+ sys.exit(1)
+
+ # Then we check if all addresses are for the same interface.
+ intfs_set = set(map(lambda i: i["interface"], ips))
+ if len(intfs_set) > 1:
+ print("Error: cluster group has addresses for different interfaces", file=sys.stderr)
+ sys.exit(1)
+
+ # If we got this far, the group is migratable.
+
+ # Extract the interface from the set -- we know there's only a single member.
+ interface = intfs_set.pop()
+
+ addresses = list(map(lambda i: i["ip"], ips))
+ vrrp_path = ['high-availability', 'vrrp', 'group', g]
+
+ # If there's already a VRRP group with exactly the same name,
+ # we probably shouldn't try to make up a unique name, just leave migration to the user...
+ if config.exists(vrrp_path):
+ print("Error: VRRP group with the same name as a cluster group already exists", file=sys.stderr)
+ sys.exit(1)
+
+ config.set(vrrp_path + ['interface'], value=interface)
+ for a in addresses:
+ config.set(vrrp_path + ['virtual-address'], value=a, replace=False)
+
+ # Take the next free VRID and assign it to the group
+ vrid = free_vrids.pop()
+ config.set(vrrp_path + ['vrid'], value=vrid)
+
+ # Convert the monitor option to VRRP ping health check
+ if config.exists(base_path + ['monitor']):
+ monitor_ip = config.return_value(base_path + ['monitor'])
+ config.set(vrrp_path + ['health-check', 'ping'], value=monitor_ip)
+
+ # Convert "auto-failback" to "no-preempt", if necessary
+ if config.exists(base_path + ['auto-failback']):
+ # It's a boolean node that requires "true" or "false"
+ # so if it exists we still need to check its value
+ auto_failback = config.return_value(base_path + ['auto-failback'])
+ if auto_failback == "false":
+ config.set(vrrp_path + ['no-preempt'])
+ else:
+ # It's "true" or we assume it is, which means preemption is desired,
+ # and in VRRP config it's the default
+ pass
+ else:
+ # The old default for that option is false
+ config.set(vrrp_path + ['no-preempt'])
+
+ # Inject settings from the global cluster config that have to be per-group in VRRP
+ if advertise_interval is not None:
+ config.set(vrrp_path + ['advertise-interval'], value=advertise_interval)
+
+ if password is not None:
+ config.set(vrrp_path + ['authentication', 'password'], value=password)
+ config.set(vrrp_path + ['authentication', 'type'], value='plaintext-password')
+
+ # Finally, clean up the old cluster node
+ config.delete(['cluster'])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/firewall/10-to-11 b/src/migration-scripts/firewall/10-to-11
index 716c5a240..b739fb139 100755
--- a/src/migration-scripts/firewall/10-to-11
+++ b/src/migration-scripts/firewall/10-to-11
@@ -181,191 +181,6 @@ if config.exists(base + ['interface']):
config.delete(base + ['interface'])
-
-### Migration of zones:
-### User interface groups
-if config.exists(base + ['zone']):
- inp_ipv4_rule = 101
- inp_ipv6_rule = 101
- fwd_ipv4_rule = 101
- fwd_ipv6_rule = 101
- out_ipv4_rule = 101
- out_ipv6_rule = 101
- local_zone = 'False'
-
- for zone in config.list_nodes(base + ['zone']):
- if config.exists(base + ['zone', zone, 'local-zone']):
- local_zone = 'True'
- # Add default-action== accept for compatibility reasons:
- config.set(base + ['ipv4', 'input', 'filter', 'default-action'], value='accept')
- config.set(base + ['ipv6', 'input', 'filter', 'default-action'], value='accept')
- config.set(base + ['ipv4', 'output', 'filter', 'default-action'], value='accept')
- config.set(base + ['ipv6', 'output', 'filter', 'default-action'], value='accept')
- for from_zone in config.list_nodes(base + ['zone', zone, 'from']):
- group_name = 'IG_' + from_zone
- if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']):
- # ipv4 input ruleset
- target_ipv4_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'name'])
- config.set(base + ['ipv4', 'input', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'input', 'filter', 'rule'])
- config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'inbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'action'], value='jump')
- config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'jump-target'], value=target_ipv4_chain)
- inp_ipv4_rule = inp_ipv4_rule + 5
- if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']):
- # ipv6 input ruleset
- target_ipv6_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name'])
- config.set(base + ['ipv6', 'input', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'input', 'filter', 'rule'])
- config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'inbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'action'], value='jump')
- config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'jump-target'], value=target_ipv6_chain)
- inp_ipv6_rule = inp_ipv6_rule + 5
-
- # Migrate: set firewall zone <zone> default-action <action>
- # Options: drop or reject. If not specified, is drop
- if config.exists(base + ['zone', zone, 'default-action']):
- local_def_action = config.return_value(base + ['zone', zone, 'default-action'])
- else:
- local_def_action = 'drop'
- config.set(base + ['ipv4', 'input', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'input', 'filter', 'rule'])
- config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'action'], value=local_def_action)
- config.set(base + ['ipv6', 'input', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'input', 'filter', 'rule'])
- config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'action'], value=local_def_action)
- if config.exists(base + ['zone', zone, 'enable-default-log']):
- config.set(base + ['ipv4', 'input', 'filter', 'rule', inp_ipv4_rule, 'log'], value='enable')
- config.set(base + ['ipv6', 'input', 'filter', 'rule', inp_ipv6_rule, 'log'], value='enable')
-
- else:
- # It's not a local zone
- group_name = 'IG_' + zone
- # Add default-action== accept for compatibility reasons:
- config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept')
- config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept')
- # intra-filtering migration. By default accept
- intra_zone_ipv4_action = 'accept'
- intra_zone_ipv6_action = 'accept'
-
- if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'action']):
- intra_zone_ipv4_action = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'action'])
- intra_zone_ipv6_action = intra_zone_ipv4_action
- else:
- if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']):
- intra_zone_ipv4_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name'])
- intra_zone_ipv4_action = 'jump'
- if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']):
- intra_zone_ipv6_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name'])
- intra_zone_ipv6_action = 'jump'
- config.set(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'inbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value=intra_zone_ipv4_action)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'inbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value=intra_zone_ipv6_action)
- if intra_zone_ipv4_action == 'jump':
- if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name']):
- intra_zone_ipv4_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'name'])
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'jump-target'], value=intra_zone_ipv4_target)
- if intra_zone_ipv6_action == 'jump':
- if config.exists(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name']):
- intra_zone_ipv6_target = config.return_value(base + ['zone', zone, 'intra-zone-filtering', 'firewall', 'ipv6-name'])
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'jump-target'], value=intra_zone_ipv6_target)
- fwd_ipv4_rule = fwd_ipv4_rule + 5
- fwd_ipv6_rule = fwd_ipv6_rule + 5
-
- if config.exists(base + ['zone', zone, 'interface']):
- # Create interface group IG_<zone>
- group_name = 'IG_' + zone
- config.set(base + ['group', 'interface-group'], value=group_name)
- config.set_tag(base + ['group', 'interface-group'])
- for iface in config.return_values(base + ['zone', zone, 'interface']):
- config.set(base + ['group', 'interface-group', group_name, 'interface'], value=iface, replace=False)
-
- if config.exists(base + ['zone', zone, 'from']):
- for from_zone in config.list_nodes(base + ['zone', zone, 'from']):
- from_group = 'IG_' + from_zone
- if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'name']):
- target_ipv4_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'name'])
- if config.exists(base + ['zone', from_zone, 'local-zone']):
- # It's from LOCAL zone -> Output filtering
- config.set(base + ['ipv4', 'output', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'output', 'filter', 'rule'])
- config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'action'], value='jump')
- config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'jump-target'], value=target_ipv4_chain)
- out_ipv4_rule = out_ipv4_rule + 5
- else:
- # It's not LOCAL zone -> forward filtering
- config.set(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'inbound-interface', 'interface-group'], value=from_group)
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value='jump')
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'jump-target'], value=target_ipv4_chain)
- fwd_ipv4_rule = fwd_ipv4_rule + 5
- if config.exists(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name']):
- target_ipv6_chain = config.return_value(base + ['zone', zone, 'from', from_zone, 'firewall', 'ipv6-name'])
- if config.exists(base + ['zone', from_zone, 'local-zone']):
- # It's from LOCAL zone -> Output filtering
- config.set(base + ['ipv6', 'output', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'output', 'filter', 'rule'])
- config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'action'], value='jump')
- config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'jump-target'], value=target_ipv6_chain)
- out_ipv6_rule = out_ipv6_rule + 5
- else:
- # It's not LOCAL zone -> forward filtering
- config.set(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'inbound-interface', 'interface-group'], value=from_group)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value='jump')
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'jump-target'], value=target_ipv6_chain)
- fwd_ipv6_rule = fwd_ipv6_rule + 5
-
- ## Now need to migrate: set firewall zone <zone> default-action <action> # action=drop if not specified.
- if config.exists(base + ['zone', zone, 'default-action']):
- def_action = config.return_value(base + ['zone', zone, 'default-action'])
- else:
- def_action = 'drop'
- config.set(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'action'], value=def_action)
- description = 'zone_' + zone + ' default-action'
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'description'], value=description)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'forward', 'filter', 'rule'])
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'outbound-interface', 'interface-group'], value=group_name)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'action'], value=def_action)
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'description'], value=description)
-
- if config.exists(base + ['zone', zone, 'enable-default-log']):
- config.set(base + ['ipv4', 'forward', 'filter', 'rule', fwd_ipv4_rule, 'log'], value='enable')
- config.set(base + ['ipv6', 'forward', 'filter', 'rule', fwd_ipv6_rule, 'log'], value='enable')
- fwd_ipv4_rule = fwd_ipv4_rule + 5
- fwd_ipv6_rule = fwd_ipv6_rule + 5
-
- # Migrate default-action (force to be drop in output chain) if local zone is defined
- if local_zone == 'True':
- # General drop in output change if needed
- config.set(base + ['ipv4', 'output', 'filter', 'rule'])
- config.set_tag(base + ['ipv4', 'output', 'filter', 'rule'])
- config.set(base + ['ipv4', 'output', 'filter', 'rule', out_ipv4_rule, 'action'], value=local_def_action)
- config.set(base + ['ipv6', 'output', 'filter', 'rule'])
- config.set_tag(base + ['ipv6', 'output', 'filter', 'rule'])
- config.set(base + ['ipv6', 'output', 'filter', 'rule', out_ipv6_rule, 'action'], value=local_def_action)
-
- config.delete(base + ['zone'])
-
-###### END migration zones
-
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
diff --git a/src/migration-scripts/interfaces/30-to-31 b/src/migration-scripts/interfaces/30-to-31
new file mode 100755
index 000000000..894106ef4
--- /dev/null
+++ b/src/migration-scripts/interfaces/30-to-31
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2023 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/>.
+#
+# Deletes Wireguard peers if they have the same public key as the router has.
+
+import json
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import BondIf
+from vyos.utils.dict import dict_to_paths_values
+
+if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
+ base = ['interfaces', 'bonding']
+
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+for bond in config.list_nodes(base):
+ member_base = base + [bond, 'member', 'interface']
+ if config.exists(member_base):
+ for interface in config.return_values(member_base):
+ if_base = ['interfaces', 'ethernet', interface]
+ if config.exists(if_base):
+ config_ethernet = json.loads(config.get_subtree(if_base).to_json())
+ eth_dict_paths = dict_to_paths_values(config_ethernet)
+ for option_path, option_value in eth_dict_paths.items():
+ # If option is allowed for changing then continue
+ converted_path = option_path.replace('-','_')
+ if converted_path in EthernetIf.get_bond_member_allowed_options():
+ continue
+ # if option is inherited from bond then continue
+ if converted_path in BondIf.get_inherit_bond_options():
+ continue
+ option_path_list = option_path.split('.')
+ config.delete(if_base + option_path_list)
+ del option_path_list[-1]
+ # delete empty node from config
+ while len(option_path_list) > 0:
+ if config.list_nodes(if_base + option_path_list):
+ break
+ config.delete(if_base + option_path_list)
+ del option_path_list[-1]
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/interfaces/31-to-32 b/src/migration-scripts/interfaces/31-to-32
new file mode 100755
index 000000000..35b397c39
--- /dev/null
+++ b/src/migration-scripts/interfaces/31-to-32
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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/>.
+#
+# T5671: change port to IANA assigned default port
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+
+if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
+ base = ['interfaces', 'vxlan']
+
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for vxlan in config.list_nodes(base):
+ if not config.exists(base + ['port']):
+ config.set(base + [vxlan, 'port'], value='8472')
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/openvpn/0-to-1 b/src/migration-scripts/openvpn/0-to-1
new file mode 100755
index 000000000..24bb38d3c
--- /dev/null
+++ b/src/migration-scripts/openvpn/0-to-1
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+# Removes outdated ciphers (DES and Blowfish) from OpenVPN configs
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['interfaces', 'openvpn']):
+ # Nothing to do
+ sys.exit(0)
+else:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
+ for i in ovpn_intfs:
+ # Remove DES and Blowfish from 'encryption cipher'
+ cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'cipher']
+ if config.exists(cipher_path):
+ cipher = config.return_value(cipher_path)
+ if cipher in ['des', 'bf128', 'bf256']:
+ config.delete(cipher_path)
+
+ ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers']
+ if config.exists(ncp_cipher_path):
+ ncp_ciphers = config.return_values(['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers'])
+ if 'des' in ncp_ciphers:
+ config.delete_value(['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers'], 'des')
+
+ # Clean up the encryption subtree if the migration procedure left it empty
+ if config.exists(['interfaces', 'openvpn', i, 'encryption']) and \
+ (config.list_nodes(['interfaces', 'openvpn', i, 'encryption']) == []):
+ config.delete(['interfaces', 'openvpn', i, 'encryption'])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/generate_tech-support_archive.py b/src/op_mode/generate_tech-support_archive.py
new file mode 100755
index 000000000..c490b0137
--- /dev/null
+++ b/src/op_mode/generate_tech-support_archive.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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 os
+import argparse
+import glob
+from datetime import datetime
+from pathlib import Path
+from shutil import rmtree
+
+from socket import gethostname
+from sys import exit
+from tarfile import open as tar_open
+from vyos.utils.process import rc_cmd
+from vyos.remote import upload
+
+def op(cmd: str) -> str:
+ """Returns a command with the VyOS operational mode wrapper."""
+ return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}'
+
+def save_stdout(command: str, file: Path) -> None:
+ rc, stdout = rc_cmd(command)
+ body: str = f'''### {command} ###
+Command: {command}
+Exit code: {rc}
+Stdout:
+{stdout}
+
+'''
+ with file.open(mode='a') as f:
+ f.write(body)
+def __rotate_logs(path: str, log_pattern:str):
+ files_list = glob.glob(f'{path}/{log_pattern}')
+ if len(files_list) > 5:
+ oldest_file = min(files_list, key=os.path.getctime)
+ os.remove(oldest_file)
+
+
+def __generate_archived_files(location_path: str) -> None:
+ """
+ Generate arhives of main directories
+ :param location_path: path to temporary directory
+ :type location_path: str
+ """
+ # Dictionary arhive_name:directory_to_arhive
+ archive_dict = {
+ 'etc': '/etc',
+ 'home': '/home',
+ 'var-log': '/var/log',
+ 'root': '/root',
+ 'tmp': '/tmp',
+ 'core-dump': '/var/core',
+ 'config': '/opt/vyatta/etc/config'
+ }
+ # Dictionary arhive_name:excluding pattern
+ archive_excludes = {
+ # Old location of archives
+ 'config': 'tech-support-archive',
+ # New locations of arhives
+ 'tmp': 'tech-support-archive'
+ }
+ for archive_name, path in archive_dict.items():
+ archive_file: str = f'{location_path}/{archive_name}.tar.gz'
+ with tar_open(name=archive_file, mode='x:gz') as tar_file:
+ if archive_name in archive_excludes:
+ tar_file.add(path, filter=lambda x: None if str(archive_excludes[archive_name]) in str(x.name) else x)
+ else:
+ tar_file.add(path)
+
+
+def __generate_main_archive_file(archive_file: str, tmp_dir_path: str) -> None:
+ """
+ Generate main arhive file
+ :param archive_file: name of arhive file
+ :type archive_file: str
+ :param tmp_dir_path: path to arhive memeber
+ :type tmp_dir_path: str
+ """
+ with tar_open(name=archive_file, mode='x:gz') as tar_file:
+ tar_file.add(tmp_dir_path, arcname=os.path.basename(tmp_dir_path))
+
+
+if __name__ == '__main__':
+ defualt_tmp_dir = '/tmp'
+ parser = argparse.ArgumentParser()
+ parser.add_argument("path", nargs='?', default=defualt_tmp_dir)
+ args = parser.parse_args()
+ location_path = args.path[:-1] if args.path[-1] == '/' else args.path
+
+ hostname: str = gethostname()
+ time_now: str = datetime.now().isoformat(timespec='seconds').replace(":", "-")
+
+ remote = False
+ tmp_path = ''
+ tmp_dir_path = ''
+ if 'ftp://' in args.path or 'scp://' in args.path:
+ remote = True
+ tmp_path = defualt_tmp_dir
+ else:
+ tmp_path = location_path
+ archive_pattern = f'_tech-support-archive_'
+ archive_file_name = f'{hostname}{archive_pattern}{time_now}.tar.gz'
+
+ # Log rotation in tmp directory
+ if tmp_path == defualt_tmp_dir:
+ __rotate_logs(tmp_path, f'*{archive_pattern}*')
+
+ # Temporary directory creation
+ tmp_dir_path = f'{tmp_path}/drops-debug_{time_now}'
+ tmp_dir: Path = Path(tmp_dir_path)
+ tmp_dir.mkdir()
+
+ report_file: Path = Path(f'{tmp_dir_path}/show_tech-support_report.txt')
+ report_file.touch()
+ try:
+
+ save_stdout(op('show tech-support report'), report_file)
+ # Generate included archives
+ __generate_archived_files(tmp_dir_path)
+
+ # Generate main archive
+ __generate_main_archive_file(f'{tmp_path}/{archive_file_name}', tmp_dir_path)
+ # Delete temporary directory
+ rmtree(tmp_dir)
+ # Upload to remote site if it is scpecified
+ if remote:
+ upload(f'{tmp_path}/{archive_file_name}', args.path)
+ print(f'Debug file is generated and located in {location_path}/{archive_file_name}')
+ except Exception as err:
+ print(f'Error during generating a debug file: {err}')
+ # cleanup
+ if tmp_dir.exists():
+ rmtree(tmp_dir)
+ finally:
+ # cleanup
+ exit()
diff --git a/src/op_mode/interfaces_wireless.py b/src/op_mode/interfaces_wireless.py
new file mode 100755
index 000000000..dfe50e2cb
--- /dev/null
+++ b/src/op_mode/interfaces_wireless.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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 re
+import sys
+import typing
+import vyos.opmode
+
+from copy import deepcopy
+from tabulate import tabulate
+from vyos.utils.process import popen
+from vyos.configquery import ConfigTreeQuery
+
+def _verify(func):
+ """Decorator checks if Wireless LAN config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ if not config.exists(['interfaces', 'wireless']):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+def _get_raw_info_data():
+ output_data = []
+
+ config = ConfigTreeQuery()
+ raw = config.get_config_dict(['interfaces', 'wireless'], effective=True,
+ get_first_key=True, key_mangling=('-', '_'))
+ for interface, interface_config in raw.items():
+ tmp = {'name' : interface}
+
+ if 'type' in interface_config:
+ tmp.update({'type' : interface_config['type']})
+ else:
+ tmp.update({'type' : '-'})
+
+ if 'ssid' in interface_config:
+ tmp.update({'ssid' : interface_config['ssid']})
+ else:
+ tmp.update({'ssid' : '-'})
+
+ if 'channel' in interface_config:
+ tmp.update({'channel' : interface_config['channel']})
+ else:
+ tmp.update({'channel' : '-'})
+
+ output_data.append(tmp)
+
+ return output_data
+
+def _get_formatted_info_output(raw_data):
+ output=[]
+ for ssid in raw_data:
+ output.append([ssid['name'], ssid['type'], ssid['ssid'], ssid['channel']])
+
+ headers = ["Interface", "Type", "SSID", "Channel"]
+ print(tabulate(output, headers, numalign="left"))
+
+def _get_raw_scan_data(intf_name):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'iw dev {intf_name} scan ap-force')
+ networks = []
+ data = {
+ 'ssid': '',
+ 'mac': '',
+ 'channel': '',
+ 'signal': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('BSS '):
+ ssid = deepcopy(data)
+ ssid['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('SSID: '):
+ # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces
+ ssid['ssid'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('signal: '):
+ # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces
+ ssid['signal'] = line.lstrip().split(':')[-1].split()[0]
+
+ elif line.lstrip().startswith('DS Parameter set: channel'):
+ # Channel can be " DS Parameter set: channel 6" , thus
+ # strip all leading whitespaces
+ ssid['channel'] = line.lstrip().split(':')[-1].split()[-1]
+ networks.append(ssid)
+ continue
+
+ return networks
+
+def _format_scan_data(raw_data):
+ output=[]
+ for ssid in raw_data:
+ output.append([ssid['mac'], ssid['ssid'], ssid['channel'], ssid['signal']])
+ headers = ["Address", "SSID", "Channel", "Signal (dbm)"]
+ return tabulate(output, headers, numalign="left")
+
+def _get_raw_station_data(intf_name):
+ # XXX: This ignores errors
+ tmp, _ = popen(f'iw dev {intf_name} station dump')
+ clients = []
+ data = {
+ 'mac': '',
+ 'signal': '',
+ 'rx_bytes': '',
+ 'rx_packets': '',
+ 'tx_bytes': '',
+ 'tx_packets': ''
+ }
+ re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
+ for line in tmp.splitlines():
+ if line.startswith('Station'):
+ client = deepcopy(data)
+ client['mac'] = re.search(re_mac, line).group()
+
+ elif line.lstrip().startswith('signal avg:'):
+ client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0]
+
+ elif line.lstrip().startswith('rx bytes:'):
+ client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('rx packets:'):
+ client['rx_packets'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx bytes:'):
+ client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip()
+
+ elif line.lstrip().startswith('tx packets:'):
+ client['tx_packets'] = line.lstrip().split(':')[-1].lstrip()
+ clients.append(client)
+ continue
+
+ return clients
+
+def _format_station_data(raw_data):
+ output=[]
+ for ssid in raw_data:
+ output.append([ssid['mac'], ssid['signal'], ssid['rx_bytes'], ssid['rx_packets'], ssid['tx_bytes'], ssid['tx_packets']])
+ headers = ["Station", "Signal", "RX bytes", "RX packets", "TX bytes", "TX packets"]
+ return tabulate(output, headers, numalign="left")
+
+@_verify
+def show_info(raw: bool):
+ info_data = _get_raw_info_data()
+ if raw:
+ return info_data
+ return _get_formatted_info_output(info_data)
+
+def show_scan(raw: bool, intf_name: str):
+ data = _get_raw_scan_data(intf_name)
+ if raw:
+ return data
+ return _format_scan_data(data)
+
+@_verify
+def show_stations(raw: bool, intf_name: str):
+ data = _get_raw_station_data(intf_name)
+ if raw:
+ return data
+ return _format_station_data(data)
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/lldp.py b/src/op_mode/lldp.py
index c287b8fa6..58cfce443 100755
--- a/src/op_mode/lldp.py
+++ b/src/op_mode/lldp.py
@@ -114,7 +114,10 @@ def _get_formatted_output(raw_data):
# Remote software platform
platform = jmespath.search('chassis.[*][0][0].descr', values)
- tmp.append(platform[:37])
+ if platform:
+ tmp.append(platform[:37])
+ else:
+ tmp.append('')
# Remote interface
interface = jmespath.search('port.descr', values)
diff --git a/src/op_mode/show-ssh-fingerprints.py b/src/op_mode/show-ssh-fingerprints.py
new file mode 100644
index 000000000..913baae46
--- /dev/null
+++ b/src/op_mode/show-ssh-fingerprints.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import glob
+import argparse
+from vyos.utils.process import cmd
+
+# Parse command line
+parser = argparse.ArgumentParser()
+parser.add_argument("--ascii", help="Show visual ASCII art representation of the public key", action="store_true")
+args = parser.parse_args()
+
+# Get list of server public keys
+publickeys = glob.glob("/etc/ssh/*.pub")
+
+if publickeys:
+ print("SSH server public key fingerprints:\n", flush=True)
+ for keyfile in publickeys:
+ if args.ascii:
+ try:
+ print(cmd("ssh-keygen -l -v -E sha256 -f " + keyfile) + "\n", flush=True)
+ # Ignore invalid public keys
+ except:
+ pass
+ else:
+ try:
+ print(cmd("ssh-keygen -l -E sha256 -f " + keyfile) + "\n", flush=True)
+ # Ignore invalid public keys
+ except:
+ pass
+else:
+ print("No SSH server public keys are found.", flush=True)
+
+sys.exit(0)
diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py
deleted file mode 100755
index 340163057..000000000
--- a/src/op_mode/show_wireless.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2023 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 re
-
-from sys import exit
-from copy import deepcopy
-
-from vyos.config import Config
-from vyos.utils.process import popen
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'")
-parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration")
-parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'")
-
-def show_brief():
- config = Config()
- if len(config.list_effective_nodes('interfaces wireless')) == 0:
- print("No Wireless interfaces configured")
- exit(0)
-
- interfaces = []
- for intf in config.list_effective_nodes('interfaces wireless'):
- config.set_level(f'interfaces wireless {intf}')
- data = { 'name': intf }
- data['type'] = config.return_effective_value('type') or '-'
- data['ssid'] = config.return_effective_value('ssid') or '-'
- data['channel'] = config.return_effective_value('channel') or '-'
- interfaces.append(data)
-
- return interfaces
-
-def ssid_scan(intf):
- # XXX: This ignores errors
- tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force')
- networks = []
- data = {
- 'ssid': '',
- 'mac': '',
- 'channel': '',
- 'signal': ''
- }
- re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
- for line in tmp.splitlines():
- if line.startswith('BSS '):
- ssid = deepcopy(data)
- ssid['mac'] = re.search(re_mac, line).group()
-
- elif line.lstrip().startswith('SSID: '):
- # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces
- ssid['ssid'] = line.lstrip().split(':')[-1].lstrip()
-
- elif line.lstrip().startswith('signal: '):
- # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces
- ssid['signal'] = line.lstrip().split(':')[-1].split()[0]
-
- elif line.lstrip().startswith('DS Parameter set: channel'):
- # Channel can be " DS Parameter set: channel 6" , thus
- # strip all leading whitespaces
- ssid['channel'] = line.lstrip().split(':')[-1].split()[-1]
- networks.append(ssid)
- continue
-
- return networks
-
-def show_clients(intf):
- # XXX: This ignores errors
- tmp, _ = popen(f'/sbin/iw dev {intf} station dump')
- clients = []
- data = {
- 'mac': '',
- 'signal': '',
- 'rx_bytes': '',
- 'rx_packets': '',
- 'tx_bytes': '',
- 'tx_packets': ''
- }
- re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})')
- for line in tmp.splitlines():
- if line.startswith('Station'):
- client = deepcopy(data)
- client['mac'] = re.search(re_mac, line).group()
-
- elif line.lstrip().startswith('signal avg:'):
- client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0]
-
- elif line.lstrip().startswith('rx bytes:'):
- client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip()
-
- elif line.lstrip().startswith('rx packets:'):
- client['rx_packets'] = line.lstrip().split(':')[-1].lstrip()
-
- elif line.lstrip().startswith('tx bytes:'):
- client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip()
-
- elif line.lstrip().startswith('tx packets:'):
- client['tx_packets'] = line.lstrip().split(':')[-1].lstrip()
- clients.append(client)
- continue
-
- return clients
-
-if __name__ == '__main__':
- args = parser.parse_args()
-
- if args.scan:
- print("Address SSID Channel Signal (dbm)")
- for network in ssid_scan(args.scan):
- print("{:<17} {:<32} {:>3} {}".format(network['mac'],
- network['ssid'],
- network['channel'],
- network['signal']))
- exit(0)
-
- elif args.brief:
- print("Interface Type SSID Channel")
- for intf in show_brief():
- print("{:<9} {:<12} {:<32} {:>3}".format(intf['name'],
- intf['type'],
- intf['ssid'],
- intf['channel']))
- exit(0)
-
- elif args.stations:
- print("Station Signal RX: bytes packets TX: bytes packets")
- for client in show_clients(args.stations):
- print("{:<17} {:>3} {:>15} {:>9} {:>15} {:>10} ".format(client['mac'],
- client['signal'], client['rx_bytes'], client['rx_packets'], client['tx_bytes'], client['tx_packets']))
-
- exit(0)
-
- else:
- parser.print_help()
- exit(1)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 66e80ced5..3a9efb73e 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -22,12 +22,14 @@ import grp
import copy
import json
import logging
+import signal
import traceback
import threading
+from time import sleep
from typing import List, Union, Callable, Dict
-import uvicorn
from fastapi import FastAPI, Depends, Request, Response, HTTPException
+from fastapi import BackgroundTasks
from fastapi.responses import HTMLResponse
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute
@@ -36,10 +38,14 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.datastructures import FormData
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
+from uvicorn import Config as UvicornConfig
+from uvicorn import Server as UvicornServer
from ariadne.asgi import GraphQL
-import vyos.config
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.configdiff import get_config_diff
from vyos.configsession import ConfigSession, ConfigSessionError
import api.graphql.state
@@ -410,12 +416,24 @@ app.router.route_class = MultipartRoute
async def validation_exception_handler(request, exc):
return error(400, str(exc.errors()[0]))
+self_ref_msg = "Requested HTTP API server configuration change; commit will be called in the background"
+
+def call_commit(s: ConfigSession):
+ try:
+ s.commit()
+ except ConfigSessionError as e:
+ s.discard()
+ if app.state.vyos_debug:
+ logger.warning(f"ConfigSessionError:\n {traceback.format_exc()}")
+ else:
+ logger.warning(f"ConfigSessionError: {e}")
+
def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
ConfigSectionModel, ConfigSectionListModel],
- request: Request):
+ request: Request, background_tasks: BackgroundTasks):
session = app.state.vyos_session
env = session.get_session_env()
- config = vyos.config.Config(session_env=env)
+ config = Config(session_env=env)
endpoint = request.url.path
@@ -470,7 +488,15 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
else:
raise ConfigSessionError(f"'{op}' is not a valid operation")
# end for
- session.commit()
+ config = Config(session_env=env)
+ d = get_config_diff(config)
+
+ if d.is_node_changed(['service', 'https']):
+ background_tasks.add_task(call_commit, session)
+ msg = self_ref_msg
+ else:
+ session.commit()
+
logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'")
except ConfigSessionError as e:
session.discard()
@@ -495,21 +521,21 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
@app.post('/configure')
def configure_op(data: Union[ConfigureModel,
- ConfigureListModel],
- request: Request):
- return _configure_op(data, request)
+ ConfigureListModel],
+ request: Request, background_tasks: BackgroundTasks):
+ return _configure_op(data, request, background_tasks)
@app.post('/configure-section')
def configure_section_op(data: Union[ConfigSectionModel,
- ConfigSectionListModel],
- request: Request):
- return _configure_op(data, request)
+ ConfigSectionListModel],
+ request: Request, background_tasks: BackgroundTasks):
+ return _configure_op(data, request, background_tasks)
@app.post("/retrieve")
async def retrieve_op(data: RetrieveModel):
session = app.state.vyos_session
env = session.get_session_env()
- config = vyos.config.Config(session_env=env)
+ config = Config(session_env=env)
op = data.op
path = " ".join(data.path)
@@ -528,10 +554,10 @@ async def retrieve_op(data: RetrieveModel):
res = session.show_config(path=data.path)
if config_format == 'json':
- config_tree = vyos.configtree.ConfigTree(res)
+ config_tree = ConfigTree(res)
res = json.loads(config_tree.to_json())
elif config_format == 'json_ast':
- config_tree = vyos.configtree.ConfigTree(res)
+ config_tree = ConfigTree(res)
res = json.loads(config_tree.to_json_ast())
elif config_format == 'raw':
pass
@@ -548,10 +574,11 @@ async def retrieve_op(data: RetrieveModel):
return success(res)
@app.post('/config-file')
-def config_file_op(data: ConfigFileModel):
+def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTasks):
session = app.state.vyos_session
-
+ env = session.get_session_env()
op = data.op
+ msg = None
try:
if op == 'save':
@@ -559,14 +586,23 @@ def config_file_op(data: ConfigFileModel):
path = data.file
else:
path = '/config/config.boot'
- res = session.save_config(path)
+ msg = session.save_config(path)
elif op == 'load':
if data.file:
path = data.file
else:
return error(400, "Missing required field \"file\"")
- res = session.migrate_and_load_config(path)
- res = session.commit()
+
+ session.migrate_and_load_config(path)
+
+ config = Config(session_env=env)
+ d = get_config_diff(config)
+
+ if d.is_node_changed(['service', 'https']):
+ background_tasks.add_task(call_commit, session)
+ msg = self_ref_msg
+ else:
+ session.commit()
else:
return error(400, f"'{op}' is not a valid operation")
except ConfigSessionError as e:
@@ -575,7 +611,7 @@ def config_file_op(data: ConfigFileModel):
logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
- return success(res)
+ return success(msg)
@app.post('/image')
def image_op(data: ImageModel):
@@ -607,7 +643,7 @@ def image_op(data: ImageModel):
return success(res)
@app.post('/container-image')
-def image_op(data: ContainerImageModel):
+def container_image_op(data: ContainerImageModel):
session = app.state.vyos_session
op = data.op
@@ -702,7 +738,7 @@ def reset_op(data: ResetModel):
# GraphQL integration
###
-def graphql_init(fast_api_app):
+def graphql_init(app: FastAPI = app):
from api.graphql.libs.token_auth import get_user_context
api.graphql.state.init()
api.graphql.state.settings['app'] = app
@@ -728,26 +764,45 @@ def graphql_init(fast_api_app):
debug=True,
introspection=in_spec))
###
+# Modify uvicorn to allow reloading server within the configsession
+###
-if __name__ == '__main__':
- # systemd's user and group options don't work, do it by hand here,
- # else no one else will be able to commit
- cfg_group = grp.getgrnam(CFG_GROUP)
- os.setgid(cfg_group.gr_gid)
+server = None
+shutdown = False
- # Need to set file permissions to 775 too so that every vyattacfg group member
- # has write access to the running config
- os.umask(0o002)
+class ApiServerConfig(UvicornConfig):
+ pass
+
+class ApiServer(UvicornServer):
+ def install_signal_handlers(self):
+ pass
+
+def reload_handler(signum, frame):
+ global server
+ logger.debug('Reload signal received...')
+ if server is not None:
+ server.handle_exit(signum, frame)
+ server = None
+ logger.info('Server stopping for reload...')
+ else:
+ logger.warning('Reload called for non-running server...')
+def shutdown_handler(signum, frame):
+ global shutdown
+ logger.debug('Shutdown signal received...')
+ server.handle_exit(signum, frame)
+ logger.info('Server shutdown...')
+ shutdown = True
+
+def initialization(session: ConfigSession, app: FastAPI = app):
+ global server
try:
server_config = load_server_config()
- except Exception as err:
- logger.critical(f"Failed to load the HTTP API server config: {err}")
+ except Exception as e:
+ logger.critical(f'Failed to load the HTTP API server config: {e}')
sys.exit(1)
- config_session = ConfigSession(os.getpid())
-
- app.state.vyos_session = config_session
+ app.state.vyos_session = session
app.state.vyos_keys = server_config['api_keys']
app.state.vyos_debug = server_config['debug']
@@ -770,14 +825,44 @@ if __name__ == '__main__':
if app.state.vyos_graphql:
graphql_init(app)
+ if not server_config['socket']:
+ config = ApiServerConfig(app,
+ host=server_config["listen_address"],
+ port=int(server_config["port"]),
+ proxy_headers=True)
+ else:
+ config = ApiServerConfig(app,
+ uds="/run/api.sock",
+ proxy_headers=True)
+ server = ApiServer(config)
+
+def run_server():
try:
- if not server_config['socket']:
- uvicorn.run(app, host=server_config["listen_address"],
- port=int(server_config["port"]),
- proxy_headers=True)
- else:
- uvicorn.run(app, uds="/run/api.sock",
- proxy_headers=True)
- except OSError as err:
- logger.critical(f"OSError {err}")
+ server.run()
+ except OSError as e:
+ logger.critical(e)
sys.exit(1)
+
+if __name__ == '__main__':
+ # systemd's user and group options don't work, do it by hand here,
+ # else no one else will be able to commit
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ # Need to set file permissions to 775 too so that every vyattacfg group member
+ # has write access to the running config
+ os.umask(0o002)
+
+ signal.signal(signal.SIGHUP, reload_handler)
+ signal.signal(signal.SIGTERM, shutdown_handler)
+
+ config_session = ConfigSession(os.getpid())
+
+ while True:
+ logger.debug('Enter main loop...')
+ if shutdown:
+ break
+ if server is None:
+ initialization(config_session)
+ server.run()
+ sleep(1)
diff --git a/src/system/uacctd_stop.py b/src/system/uacctd_stop.py
new file mode 100755
index 000000000..a1b57335b
--- /dev/null
+++ b/src/system/uacctd_stop.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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/>.
+
+# Control pmacct daemons in a tricky way.
+# Pmacct has signal processing in a main loop, together with packet
+# processing. Because of this, while it is waiting for packets, it cannot
+# handle the control signal. We need to start the systemctl command and then
+# send some packets to pmacct to wake it up
+
+from argparse import ArgumentParser
+from socket import socket, AF_INET, SOCK_DGRAM
+from sys import exit
+from time import sleep
+
+from psutil import Process
+
+
+def stop_process(pid: int, timeout: int) -> None:
+ """Send a signal to uacctd
+ and then send packets to special address predefined in a firewall
+ to unlock main loop in uacctd and finish the process properly
+
+ Args:
+ pid (int): uacctd PID
+ timeout (int): seconds to wait for a process end
+ """
+ # find a process
+ uacctd = Process(pid)
+ uacctd.terminate()
+
+ # create a socket
+ trigger = socket(AF_INET, SOCK_DGRAM)
+
+ first_cycle: bool = True
+ while uacctd.is_running() and timeout:
+ print('sending a packet to uacctd...')
+ trigger.sendto(b'WAKEUP', ('127.0.254.0', 1))
+ # do not sleep during first attempt
+ if not first_cycle:
+ sleep(1)
+ timeout -= 1
+ first_cycle = False
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument('process_id',
+ type=int,
+ help='PID file of uacctd core process')
+ parser.add_argument('timeout',
+ type=int,
+ help='time to wait for process end')
+ args = parser.parse_args()
+ stop_process(args.process_id, args.timeout)
+ exit()