diff options
67 files changed, 2161 insertions, 821 deletions
@@ -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/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> <file> <scp://user:passwd@host> <ftp://user:passwd@host></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() |