diff options
186 files changed, 5274 insertions, 2664 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,13 +60,13 @@ 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 - # options are provided from the script itself + # XXX: ping, traceroute and mtr must be able to recursivly call themselves as the + # options are provided from the scripts themselves ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ ln -s ../node.tag $(OP_TMPL_DIR)/traceroute/node.tag/node.tag/ + ln -s ../node.tag $(OP_TMPL_DIR)/mtr/node.tag/node.tag/ # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 6c86642c7..681cf3ef9 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -1,52 +1,59 @@ { - "conntrack": {"conntrack_sync": ["conntrack_sync"]}, - "firewall": { - "conntrack": ["conntrack"], - "conntrack_sync": ["conntrack_sync"], - "group_resync": ["conntrack", "nat", "policy-route"] - }, - "http_api": {"https": ["https"]}, - "load_balancing_wan": { - "conntrack": ["conntrack"], - "conntrack_sync": ["conntrack_sync"] - }, - "nat": { - "conntrack": ["conntrack"], - "conntrack_sync": ["conntrack_sync"] - }, - "nat66": { - "conntrack": ["conntrack"], - "conntrack_sync": ["conntrack_sync"] - }, - "pki": { - "ethernet": ["interfaces-ethernet"], - "openvpn": ["interfaces-openvpn"], - "https": ["https"], - "ipsec": ["vpn_ipsec"], - "openconnect": ["vpn_openconnect"], - "sstp": ["vpn_sstp"] - }, - "qos": { - "bonding": ["interfaces-bonding"], - "bridge": ["interfaces-bridge"], - "dummy": ["interfaces-dummy"], - "ethernet": ["interfaces-ethernet"], - "geneve": ["interfaces-geneve"], - "input": ["interfaces-input"], - "l2tpv3": ["interfaces-l2tpv3"], - "loopback": ["interfaces-loopback"], - "macsec": ["interfaces-macsec"], - "openvpn": ["interfaces-openvpn"], - "pppoe": ["interfaces-pppoe"], - "pseudo-ethernet": ["interfaces-pseudo-ethernet"], - "tunnel": ["interfaces-tunnel"], - "vti": ["interfaces-vti"], - "vxlan": ["interfaces-vxlan"], - "wireguard": ["interfaces-wireguard"], - "wireless": ["interfaces-wireless"], - "wwan": ["interfaces-wwan"] - }, - "vpp": { - "ethernet": ["interfaces-ethernet"] - } + "conntrack": { + "conntrack_sync": ["conntrack_sync"] + }, + "firewall": { + "conntrack": ["conntrack"], + "conntrack_sync": ["conntrack_sync"], + "group_resync": ["conntrack", "nat", "policy-route"] + }, + "http_api": { + "https": ["https"] + }, + "interfaces_bonding": { + "ethernet": ["interfaces-ethernet"] + }, + "interfaces_bridge": { + "vxlan": ["interfaces-vxlan"] + }, + "load_balancing_wan": { + "conntrack": ["conntrack"], + "conntrack_sync": ["conntrack_sync"] + }, + "nat": { + "conntrack": ["conntrack"], + "conntrack_sync": ["conntrack_sync"] + }, + "nat66": { + "conntrack": ["conntrack"], + "conntrack_sync": ["conntrack_sync"] + }, + "pki": { + "ethernet": ["interfaces-ethernet"], + "openvpn": ["interfaces-openvpn"], + "https": ["https"], + "ipsec": ["vpn_ipsec"], + "openconnect": ["vpn_openconnect"], + "sstp": ["vpn_sstp"] + }, + "qos": { + "bonding": ["interfaces-bonding"], + "bridge": ["interfaces-bridge"], + "dummy": ["interfaces-dummy"], + "ethernet": ["interfaces-ethernet"], + "geneve": ["interfaces-geneve"], + "input": ["interfaces-input"], + "l2tpv3": ["interfaces-l2tpv3"], + "loopback": ["interfaces-loopback"], + "macsec": ["interfaces-macsec"], + "openvpn": ["interfaces-openvpn"], + "pppoe": ["interfaces-pppoe"], + "pseudo-ethernet": ["interfaces-pseudo-ethernet"], + "tunnel": ["interfaces-tunnel"], + "vti": ["interfaces-vti"], + "vxlan": ["interfaces-vxlan"], + "wireguard": ["interfaces-wireguard"], + "wireless": ["interfaces-wireless"], + "wwan": ["interfaces-wwan"] + } } diff --git a/data/templates/accel-ppp/config_shaper_radius.j2 b/data/templates/accel-ppp/config_shaper_radius.j2 index 0cf6a6a92..fcd68f69e 100644 --- a/data/templates/accel-ppp/config_shaper_radius.j2 +++ b/data/templates/accel-ppp/config_shaper_radius.j2 @@ -1,6 +1,7 @@ {% if authentication.mode is vyos_defined('radius') or shaper is vyos_defined %} [shaper] verbose=1 +down-limiter=tbf {% if authentication.radius.rate_limit.enable is vyos_defined %} attr={{ authentication.radius.rate_limit.attribute }} {% if authentication.radius.rate_limit.vendor is vyos_defined %} @@ -13,7 +14,6 @@ rate-multiplier={{ authentication.radius.rate_limit.multiplier }} {% if shaper is vyos_defined %} {% if shaper.fwmark is vyos_defined %} fwmark={{ shaper.fwmark }} -down-limiter=htb {% endif %} {% endif %} {% endif %}
\ No newline at end of file diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index f59428509..555a033d3 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -14,6 +14,11 @@ ippool [core] thread-count={{ thread_count }} +[common] +{% if max_concurrent_sessions is vyos_defined %} +max-starting={{ max_concurrent_sessions }} +{% endif %} + [log] syslog=accel-ipoe,daemon copy=1 diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index a2f9c9fc7..b089d3e71 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -20,6 +20,11 @@ ipv6_dhcp [core] thread-count={{ thread_cnt }} +[common] +{% if max_concurrent_sessions is vyos_defined %} +max-starting={{ max_concurrent_sessions }} +{% endif %} + [log] syslog=accel-l2tp,daemon copy=1 diff --git a/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2 index dd53edd28..e1ae3660e 100644 --- a/data/templates/accel-ppp/pppoe.config.j2 +++ b/data/templates/accel-ppp/pppoe.config.j2 @@ -62,10 +62,13 @@ wins{{ loop.index }}={{ server }} {# Common chap-secrets and RADIUS server/option definitions #} {% include 'accel-ppp/config_chap_secrets_radius.j2' %} -{% if session_control is vyos_defined and session_control is not vyos_defined('disable') %} [common] +{% if session_control is vyos_defined and session_control is not vyos_defined('disable') %} single-session={{ session_control }} {% endif %} +{% if max_concurrent_sessions is vyos_defined %} +max-starting={{ max_concurrent_sessions }} +{% endif %} [ppp] verbose=1 diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2 index 0082e55bf..46a9f933a 100644 --- a/data/templates/accel-ppp/pptp.config.j2 +++ b/data/templates/accel-ppp/pptp.config.j2 @@ -16,6 +16,11 @@ ippool [core] thread-count={{ thread_cnt }} +[common] +{% if max_concurrent_sessions is vyos_defined %} +max-starting={{ max_concurrent_sessions }} +{% endif %} + [log] syslog=accel-pptp,daemon copy=1 diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index 7ee28dd21..cf1d23f54 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -16,6 +16,9 @@ thread-count={{ thread_count }} [common] single-session=replace +{% if max_concurrent_sessions is vyos_defined %} +max-starting={{ max_concurrent_sessions }} +{% endif %} [log] syslog=accel-sstp,daemon diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 6e77abdb5..356b8d0d0 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -13,15 +13,15 @@ web-skip{{ ipv }}='{{ web_options.skip }}', \ if{{ ipv }}={{ address }}, \ {% endif %} {% endfor %} -{# Other service options #} +{# Other service options with special treatment for password #} {% for k,v in kwargs.items() if v is vyos_defined %} -{{ k | replace('_', '-') }}={{ v }}{{ ',' if not loop.last }} \ +{{ k | replace('_', '-') }}={{ "'%s'" % (v) if k == 'password' else v }}{{ ',' if not loop.last }} \ {% endfor %} {# Actual hostname for the service #} {{ host }} {% endmacro %} ### Autogenerated by dns_dynamic.py ### -daemon={{ timeout }} +daemon={{ interval }} syslog=yes ssl=yes pid={{ config_file | replace('.conf', '.pid') }} @@ -29,7 +29,7 @@ cache={{ config_file | replace('.conf', '.cache') }} {# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #} web=googledomains {# ddclient default (use=ip) results in confusing warning message in log #} -use=no +use=disabled {% if address is vyos_defined %} {% for address, service_cfg in address.items() %} 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/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index d724dbd79..6f81174ac 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -373,6 +373,26 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if afi_config.advertise_svi_ip is vyos_defined %} advertise-svi-ip {% endif %} +{% if afi_config.default_originate.ipv4 is vyos_defined %} + default-originate ipv4 +{% endif %} +{% if afi_config.default_originate.ipv6 is vyos_defined %} + default-originate ipv6 +{% endif %} +{% if afi_config.disable_ead_evi_rx is vyos_defined %} + disable-ead-evi-rx +{% endif %} +{% if afi_config.disable_ead_evi_tx is vyos_defined %} + disable-ead-evi-tx +{% endif %} +{% if afi_config.ead_es_frag.evi_limit is vyos_defined %} + ead-es-frag evi-limit {{ afi_config.ead_es_frag.evi_limit }} +{% endif %} +{% if afi_config.ead_es_route_target.export is vyos_defined %} +{% for route_target in afi_config.ead_es_route_target.export %} + ead-es-route-target export {{ route_target }} +{% endfor %} +{% endif %} {% if afi_config.rt_auto_derive is vyos_defined %} autort rfc8365-compatible {% endif %} diff --git a/data/templates/frr/evpn.mh.frr.j2 b/data/templates/frr/evpn.mh.frr.j2 new file mode 100644 index 000000000..03aaac44b --- /dev/null +++ b/data/templates/frr/evpn.mh.frr.j2 @@ -0,0 +1,16 @@ +! +interface {{ ifname }} +{% if evpn.es_df_pref is vyos_defined %} + evpn mh es-df-pref {{ evpn.es_df_pref }} +{% endif %} +{% if evpn.es_id is vyos_defined %} + evpn mh es-id {{ evpn.es_id }} +{% endif %} +{% if evpn.es_sys_mac is vyos_defined %} + evpn mh es-sys-mac {{ evpn.es_sys_mac }} +{% endif %} +{% if evpn.uplink is vyos_defined %} + evpn mh uplink +{% endif %} +exit +! diff --git a/data/templates/frr/isisd.frr.j2 b/data/templates/frr/isisd.frr.j2 index dbb8c7305..1e1cc3c27 100644 --- a/data/templates/frr/isisd.frr.j2 +++ b/data/templates/frr/isisd.frr.j2 @@ -165,6 +165,48 @@ advertise-passive-only {% endfor %} {% endfor %} {% endif %} +{% if fast_reroute.lfa is vyos_defined %} +{% if fast_reroute.lfa.local is vyos_defined %} +{% if fast_reroute.lfa.local.load_sharing.disable.level_1 is vyos_defined %} + fast-reroute load-sharing disable level-1 +{% elif fast_reroute.lfa.local.load_sharing.disable.level_2 is vyos_defined %} + fast-reroute load-sharing disable level-2 +{% elif fast_reroute.lfa.local.load_sharing.disable is vyos_defined %} + fast-reroute load-sharing disable +{% endif %} +{% if fast_reroute.lfa.local.priority_limit is vyos_defined %} +{% for priority, priority_limit_options in fast_reroute.lfa.local.priority_limit.items() %} +{% for level in priority_limit_options %} + fast-reroute priority-limit {{ priority }} {{ level | replace('_', '-') }} +{% endfor %} +{% endfor %} +{% endif %} +{% if fast_reroute.lfa.local.tiebreaker is vyos_defined %} +{% for tiebreaker, tiebreaker_options in fast_reroute.lfa.local.tiebreaker.items() %} +{% for index, index_options in tiebreaker_options.items() %} +{% for index_value, index_value_options in index_options.items() %} +{% for level in index_value_options %} + fast-reroute lfa tiebreaker {{ tiebreaker | replace('_', '-') }} index {{ index_value }} {{ level | replace('_', '-') }} +{% endfor %} +{% endfor %} +{% endfor %} +{% endfor %} +{% endif %} +{% endif %} +{% if fast_reroute.lfa.remote.prefix_list is vyos_defined %} +{% for prefix_list, prefix_list_options in fast_reroute.lfa.remote.prefix_list.items() %} +{% if prefix_list_options.level_1 is vyos_defined %} +fast-reroute remote-lfa prefix-list {{ prefix_list }} level-1 +{% endif %} +{% if prefix_list_options.level_2 is vyos_defined %} +fast-reroute remote-lfa prefix-list {{ prefix_list }} level-2 +{% endif %} +{% if prefix_list is vyos_defined and prefix_list_options.level_1 is not vyos_defined and prefix_list_options.level_2 is not vyos_defined %} +fast-reroute remote-lfa prefix-list {{ prefix_list }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} {% if redistribute.ipv4 is vyos_defined %} {% for protocol, protocol_options in redistribute.ipv4.items() %} {% for level, level_config in protocol_options.items() %} 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/mdns-repeater/override.conf.j2 b/data/templates/mdns-repeater/override.conf.j2 new file mode 100644 index 000000000..8c8187489 --- /dev/null +++ b/data/templates/mdns-repeater/override.conf.j2 @@ -0,0 +1,7 @@ +[Unit] +After=vyos-router.service +ConditionPathExists={{ config_file }} + +[Service] +ExecStart= +ExecStart=/usr/sbin/avahi-daemon --syslog --file {{ config_file }} diff --git a/data/templates/ndppd/ndppd.conf.j2 b/data/templates/ndppd/ndppd.conf.j2 index 120fa0a64..1297f36be 100644 --- a/data/templates/ndppd/ndppd.conf.j2 +++ b/data/templates/ndppd/ndppd.conf.j2 @@ -17,12 +17,12 @@ {% set global = namespace(ndppd_interfaces = [],ndppd_prefixs = []) %} {% if source.rule is vyos_defined %} {% for rule, config in source.rule.items() if config.disable is not defined %} -{% if config.outbound_interface is vyos_defined %} -{% if config.outbound_interface not in global.ndppd_interfaces %} -{% set global.ndppd_interfaces = global.ndppd_interfaces + [config.outbound_interface] %} +{% if config.outbound_interface.name is vyos_defined %} +{% if config.outbound_interface.name not in global.ndppd_interfaces %} +{% set global.ndppd_interfaces = global.ndppd_interfaces + [config.outbound_interface.name] %} {% endif %} {% if config.translation.address is vyos_defined and config.translation.address | is_ip_network %} -{% set global.ndppd_prefixs = global.ndppd_prefixs + [{'interface':config.outbound_interface,'rule':config.translation.address}] %} +{% set global.ndppd_prefixs = global.ndppd_prefixs + [{'interface':config.outbound_interface.name,'rule':config.translation.address}] %} {% endif %} {% endif %} {% endfor %} diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2 index 2eb9416fe..c02411904 100644 --- a/data/templates/openvpn/server.conf.j2 +++ b/data/templates/openvpn/server.conf.j2 @@ -79,7 +79,7 @@ server {{ subnet | address_from_cidr }} {{ subnet | netmask_from_cidr }} {{ 'nop {% if server.push_route is vyos_defined %} {% for route, route_config in server.push_route.items() %} {% if route | is_ipv4 %} -push "route {{ route | address_from_cidr }} {{ route | netmask_from_cidr }} {{ subnet | first_host_address ~ ' ' ~ route_config.metric if route_config.metric is vyos_defined }}" +push "route {{ route | address_from_cidr }} {{ route | netmask_from_cidr }} {{ 'vpn_gateway' ~ ' ' ~ route_config.metric if route_config.metric is vyos_defined }}" {% elif route | is_ipv6 %} push "route-ipv6 {{ route }}" {% endif %} @@ -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/data/templates/pppoe/peer.j2 b/data/templates/pppoe/peer.j2 index f30cefe63..2a99fcb2a 100644 --- a/data/templates/pppoe/peer.j2 +++ b/data/templates/pppoe/peer.j2 @@ -50,7 +50,7 @@ ifname {{ ifname }} ipparam {{ ifname }} debug mtu {{ mtu }} -mru {{ mtu }} +mru {{ mru }} {% if authentication is vyos_defined %} {{ 'user "' + authentication.username + '"' if authentication.username is vyos_defined }} diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2 index dff904129..8ca167803 100644 --- a/data/templates/rsyslog/rsyslog.conf.j2 +++ b/data/templates/rsyslog/rsyslog.conf.j2 @@ -15,7 +15,7 @@ $outchannel global,/var/log/messages,262144,/usr/sbin/logrotate {{ logrotate }} {% if global.facility is vyos_defined %} {% set tmp = [] %} {% for facility, facility_options in global.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} +{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} {% endfor %} {{ tmp | join(';') }} :omfile:$global {% endif %} @@ -27,7 +27,7 @@ $outchannel global,/var/log/messages,262144,/usr/sbin/logrotate {{ logrotate }} $outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archive.size }},/usr/sbin/logrotate {{ logrotate }} {% if file_options.facility is vyos_defined %} {% for facility, facility_options in file_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} +{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} {% endfor %} {% endif %} {{ tmp | join(';') }} :omfile:${{ file }} @@ -38,7 +38,7 @@ $outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archiv # Console logging {% set tmp = [] %} {% for facility, facility_options in console.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} +{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} {% endfor %} {{ tmp | join(';') }} /dev/console {% endif %} @@ -49,7 +49,7 @@ $outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archiv {% set tmp = [] %} {% if host_options.facility is vyos_defined %} {% for facility, facility_options in host_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} +{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} {% endfor %} {% endif %} {% if host_options.protocol is vyos_defined('tcp') %} @@ -70,7 +70,7 @@ $outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archiv {% set tmp = [] %} {% if user_options.facility is vyos_defined %} {% for facility, facility_options in user_options.facility.items() %} -{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level) %} +{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %} {% endfor %} {% endif %} {{ tmp | join(';') }} :omusrmsg:{{ username }} diff --git a/data/templates/snmp/etc.snmpd.conf.j2 b/data/templates/snmp/etc.snmpd.conf.j2 index 9d78d479a..3db8c4d7b 100644 --- a/data/templates/snmp/etc.snmpd.conf.j2 +++ b/data/templates/snmp/etc.snmpd.conf.j2 @@ -56,6 +56,16 @@ SysDescr {{ description }} {% endif %} agentaddress unix:/run/snmpd.socket{{ ',' ~ options | join(',') if options is vyos_defined }} +{% if mib is vyos_defined %} +# Interface MIB limits +{% if mib.interface_max is vyos_defined %} +ifmib_max_num_ifaces {{ mib.interface_max }} +{% endif %} +{% if mib.interface is vyos_defined %} +include_ifmib_iface_prefix {{ mib.interface | join(' ') }} +{% endif %} +{% endif %} + # SNMP communities {% if community is vyos_defined %} {% for comm, comm_config in community.items() %} diff --git a/data/templates/telegraf/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 5852d6232..02a9656da 100644 --- a/data/templates/telegraf/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 @@ -89,7 +89,7 @@ ignore_fs = ["devtmpfs", "devfs"] [[inputs.diskio]] [[inputs.mem]] -[[inputs.net]] +[[inputs.nstat]] [[inputs.system]] [[inputs.netstat]] [[inputs.processes]] diff --git a/data/templates/vpp/override.conf.j2 b/data/templates/vpp/override.conf.j2 deleted file mode 100644 index a2c2b04ed..000000000 --- a/data/templates/vpp/override.conf.j2 +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -After= -After=vyos-router.service -ConditionPathExists= -ConditionPathExists=/run/vpp/vpp.conf - -[Service] -EnvironmentFile= -ExecStart= -ExecStart=/usr/bin/vpp -c /run/vpp/vpp.conf -WorkingDirectory= -WorkingDirectory=/run/vpp -Restart=always -RestartSec=10 diff --git a/data/templates/vpp/startup.conf.j2 b/data/templates/vpp/startup.conf.j2 deleted file mode 100644 index f33539fba..000000000 --- a/data/templates/vpp/startup.conf.j2 +++ /dev/null @@ -1,116 +0,0 @@ -# Generated by /usr/libexec/vyos/conf_mode/vpp.py - -unix { - nodaemon - log /var/log/vpp.log - full-coredump - cli-listen /run/vpp/cli.sock - gid vpp - # exec /etc/vpp/bootstrap.vpp -{% if unix is vyos_defined %} -{% if unix.poll_sleep_usec is vyos_defined %} - poll-sleep-usec {{ unix.poll_sleep_usec }} -{% endif %} -{% endif %} -} - -{% if cpu is vyos_defined %} -cpu { -{% if cpu.main_core is vyos_defined %} - main-core {{ cpu.main_core }} -{% endif %} -{% if cpu.corelist_workers is vyos_defined %} - corelist-workers {{ cpu.corelist_workers | join(',') }} -{% endif %} -{% if cpu.skip_cores is vyos_defined %} - skip-cores {{ cpu.skip_cores }} -{% endif %} -{% if cpu.workers is vyos_defined %} - workers {{ cpu.workers }} -{% endif %} -} -{% endif %} - -{# ip heap-size does not work now (23.06-rc2~1-g3a4e62ad4) #} -{# vlib_call_all_config_functions: unknown input `ip heap-size 32M ' #} -{% if ip is vyos_defined %} -#ip { -#{% if ip.heap_size is vyos_defined %} -# heap-size {{ ip.heap_size }}M -#{% endif %} -#} -{% endif %} - -{% if ip6 is vyos_defined %} -ip6 { -{% if ip6.hash_buckets is vyos_defined %} - hash-buckets {{ ip6.hash_buckets }} -{% endif %} -{% if ip6.heap_size is vyos_defined %} - heap-size {{ ip6.heap_size }}M -{% endif %} -} -{% endif %} - -{% if l2learn is vyos_defined %} -l2learn { -{% if l2learn.limit is vyos_defined %} - limit {{ l2learn.limit }} -{% endif %} -} -{% endif %} - -{% if logging is vyos_defined %} -logging { -{% if logging.default_log_level is vyos_defined %} - default-log-level {{ logging.default_log_level }} -{% endif %} -} -{% endif %} - -{% if physmem is vyos_defined %} -physmem { -{% if physmem.max_size is vyos_defined %} - max-size {{ physmem.max_size.upper() }} -{% endif %} -} -{% endif %} - -plugins { - path /usr/lib/x86_64-linux-gnu/vpp_plugins/ - plugin default { disable } - plugin dpdk_plugin.so { enable } - plugin linux_cp_plugin.so { enable } - plugin linux_nl_plugin.so { enable } -} - -linux-cp { - lcp-sync - lcp-auto-subint -} - -dpdk { - # Whitelist the fake PCI address 0000:00:00.0 - # This prevents all devices from being added to VPP-DPDK by default - dev 0000:00:00.0 -{% for iface, iface_config in interface.items() %} -{% if iface_config.pci is vyos_defined %} - dev {{ iface_config.pci }} { - name {{ iface }} -{% if iface_config.num_rx_desc is vyos_defined %} - num-rx-desc {{ iface_config.num_rx_desc }} -{% endif %} -{% if iface_config.num_tx_desc is vyos_defined %} - num-tx-desc {{ iface_config.num_tx_desc }} -{% endif %} -{% if iface_config.num_rx_queues is vyos_defined %} - num-rx-queues {{ iface_config.num_rx_queues }} -{% endif %} -{% if iface_config.num_tx_queues is vyos_defined %} - num-tx-queues {{ iface_config.num_tx_queues }} -{% endif %} - } -{% endif %} -{% endfor %} - uio-bind-force -} diff --git a/debian/control b/debian/control index 735733956..98c064417 100644 --- a/debian/control +++ b/debian/control @@ -11,15 +11,17 @@ Build-Depends: libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), - python3-coverage, +# For generating command definitions python3-lxml, + python3-xmltodict, +# For running tests + python3-coverage, python3-netifaces, python3-nose, python3-jinja2, python3-psutil, python3-setuptools, python3-sphinx, - python3-xmltodict, quilt, whois Standards-Version: 3.9.6 @@ -31,107 +33,20 @@ Pre-Depends: libpam-tacplus [amd64], libpam-radius-auth [amd64] Depends: +## Fundamentals ${python3:Depends} (>= 3.10), - aardvark-dns, - accel-ppp, - auditd, - avahi-daemon, - aws-gwlbtun, - beep, - bmon, - bsdmainutils, - charon-systemd, - conntrack, - conntrackd, - conserver-client, - conserver-server, - console-data, - cron, - curl, - dbus, - ddclient (>= 3.9.1), - dropbear, - easy-rsa, - etherwake, - ethtool, - fdisk, - fastnetmon [amd64], - file, - frr (>= 7.5), - frr-pythontools, - frr-rpki-rtrlib, - frr-snmp, - fuse-overlayfs, - libpam-google-authenticator, - grc, - haproxy, - hostapd, - hsflowd, - hvinfo, - igmpproxy, - ipaddrcheck, - iperf, - iperf3, - iproute2 (>= 6.0.0), - iptables, - iputils-arping, - isc-dhcp-client, - isc-dhcp-relay, - isc-dhcp-server, - iw, - keepalived (>=2.0.5), - lcdproc, - lcdproc-extra-drivers, - libatomic1, - libauparse0, - libcharon-extra-plugins (>=5.9), - libcharon-extauth-plugins (>=5.9), - libndp-tools, - libnetfilter-conntrack3, - libnfnetlink0, - libqmi-utils, - libstrongswan-extra-plugins (>=5.9), - libstrongswan-standard-plugins (>=5.9), - libvppinfra [amd64], libvyosconfig0, - linux-cpupower, - lldpd, - lm-sensors, - lsscsi, - minisign, - modemmanager, - mtr-tiny, - ndisc6, - ndppd, - netavark, - netplug, - nfct, - nftables (>= 0.9.3), - nginx-light, - chrony, - nvme-cli, - ocserv, - opennhrp, - openssh-server, - openssl, - openvpn, - openvpn-auth-ldap, - openvpn-auth-radius, - openvpn-otp, - owamp-client, - owamp-server, - pciutils, - pdns-recursor, - pmacct (>= 1.6.0), - podman, - pppoe, - procps, + vyatta-bash, + vyatta-cfg, + vyos-http-api-tools, + vyos-utils, +## End of Fundamentals +## Python libraries used in multiple modules and scripts python3, python3-certbot-nginx, python3-cryptography, python3-hurry.filesize, python3-inotify, - python3-isc-dhcp-leases, python3-jinja2, python3-jmespath, python3-netaddr, @@ -144,57 +59,250 @@ Depends: python3-pyudev, python3-six, python3-tabulate, - python3-vici (>= 5.7.2), python3-voluptuous, - python3-vpp-api [amd64], python3-xmltodict, python3-zmq, +## End of Python libraries +## Basic System services and utilities + sudo, + systemd, + bsdmainutils, + openssl, + curl, + dbus, + file, + iproute2 (>= 6.0.0), + linux-cpupower, +# ipaddrcheck is widely used in IP value validators + ipaddrcheck, + ethtool, + fdisk, + lm-sensors, + procps, + netplug, + sed, + ssl-cert, + tuned, + beep, + wide-dhcpv6-client, +# Generic colorizer + grc, +## End of System services and utilities +## For the installer +# Image signature verification tool + minisign, +# Live filesystem tools + squashfs-tools, + fuse-overlayfs, +## End installer + auditd, + iputils-arping, + isc-dhcp-client, +# For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" + accel-ppp, +# End "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" + avahi-daemon, + conntrack, + conntrackd, +## Conf mode features +# For "interfaces wireless" + hostapd, + hsflowd, + iw, + wireless-regdb, + wpasupplicant (>= 0.6.7), +# End "interfaces wireless" +# For "interfaces wwan" + modemmanager, + usb-modeswitch, + libqmi-utils, +# End "interfaces wwan" +# For "interfaces openvpn" + openvpn, + openvpn-auth-ldap, + openvpn-auth-radius, + openvpn-otp, + libpam-google-authenticator, +# End "interfaces openvpn" +# For "interfaces wireguard" + wireguard-tools, qrencode, +# End "interfaces wireguard" +# For "interfaces pppoe" + pppoe, +# End "interfaces pppoe" +# For "interfaces sstpc" + sstp-client, +# End "interfaces sstpc" +# For "protocols *" + frr (>= 7.5), + frr-pythontools, + frr-rpki-rtrlib, + frr-snmp, +# End "protocols *" +# For "protocols nhrp" (part of DMVPN) + opennhrp, +# End "protocols nhrp" +# For "protocols igmp-proxy" + igmpproxy, +# End "protocols igmp-proxy" +# For "service console-server" + conserver-client, + conserver-server, + console-data, + dropbear, +# End "service console-server" +# For "set service aws glb" + aws-gwlbtun, +# For "service dns dynamic" + ddclient (>= 3.11.1), +# End "service dns dynamic" +# # For "service ids" + fastnetmon [amd64], +# End "service ids" +# For "service router-advert" radvd, +# End "service route-advert" +# For "high-availability reverse-proxy" + haproxy, +# End "high-availability reverse-proxy" +# For "service dhcp-relay" + isc-dhcp-relay, +# For "service dhcp-server" + isc-dhcp-server, + python3-isc-dhcp-leases, +# End "service dhcp-server" +# For "service lldp" + lldpd, +# End "service lldp" +# For "service https" + nginx-light, +# End "service https" +# For "service ssh" + openssh-server, + sshguard, +# End "service ssh" +# For "service salt-minion" salt-minion, - sed, - smartmontools, +# End "service salt-minion" +# For "service snmp" snmp, snmpd, - squashfs-tools, +# End "service snmp" +# For "service upnp" + miniupnpd-nftables, +# End "service upnp" +# For "service webproxy" squid, squidclient, squidguard, - sshguard, - ssl-cert, - sstp-client, - strongswan (>= 5.9), - strongswan-swanctl (>= 5.9), - stunnel4, - sudo, - systemd, +# End "service webproxy" +# For "service monitoring telegraf" telegraf (>= 1.20), - tcpdump, - tcptraceroute, - telnet, +# End "service monitoring telegraf" +# For "service monitoring zabbix-agent" + zabbix-agent2, +# End "service monitoring zabbix-agent" +# For "service tftp-server" tftpd-hpa, - traceroute, - tuned, +# End "service tftp-server" +# For "service dns forwarding" + pdns-recursor, +# End "service dns forwarding" +# For "service sla owamp" + owamp-client, + owamp-server, +# End "service sla owamp" +# For "service sla twamp" twamp-client, twamp-server, +# End "service sla twamp" +# For "service broadcast-relay" udp-broadcast-relay, - uidmap, - usb-modeswitch, - usbutils, - vpp [amd64], - vpp-plugin-core [amd64], - vpp-plugin-dpdk [amd64], - vyatta-bash, - vyatta-cfg, - vyos-http-api-tools, - vyos-utils, - wide-dhcpv6-client, - wireguard-tools, - wireless-regdb, - wpasupplicant (>= 0.6.7), - zabbix-agent2, +# End "service broadcast-relay" +# For "high-availability vrrp" + keepalived (>=2.0.5), +# End "high-availability-vrrp" +# For "system task-scheduler" + cron, +# End "system task-scheduler" +# For "system lcd" + lcdproc, + lcdproc-extra-drivers, +# End "system lcd" +# For firewall + libndp-tools, + libnetfilter-conntrack3, + libnfnetlink0, + nfct, + nftables (>= 0.9.3), +# For "vpn ipsec" + strongswan (>= 5.9), + strongswan-swanctl (>= 5.9), + charon-systemd, + libcharon-extra-plugins (>=5.9), + libcharon-extauth-plugins (>=5.9), + libstrongswan-extra-plugins (>=5.9), + libstrongswan-standard-plugins (>=5.9), + python3-vici (>= 5.7.2), +# End "vpn ipsec" +# For nat66 ndppd, - miniupnpd-nftables +# End nat66 +# For "system ntp" + chrony, +# End "system ntp" +# For "vpn openconnect" + ocserv, +# End "vpn openconnect" +# For "set system flow-accounting" + pmacct (>= 1.6.0), +# End "set system flow-accounting" +# For container + podman, + netavark, + aardvark-dns, +# iptables is only used for containers now, not the the firewall CLI + iptables, +# End container +## End Configuration mode +## Operational mode +# Used for hypervisor model in "run show version" + hvinfo, +# For "run traceroute" + traceroute, +# For "run monitor traffic" + tcpdump, +# End "run monitor traffic" +# For "run show hardware storage smart" + smartmontools, +# For "run show hardware scsi" + lsscsi, +# For "run show hardware pci" + pciutils, +# For "show hardware usb" + usbutils, +# For "run show hardware storage nvme" + nvme-cli, +# For "run monitor bandwidth-test" + iperf, + iperf3, +# End "run monitor bandwidth-test" +# For "run wake-on-lan" + etherwake, +# For "run force ipv6-nd" + ndisc6, +# For "run monitor bandwidth" + bmon, +# End Operational mode +## Optional utilities + easy-rsa, + tcptraceroute, + mtr-tiny, + telnet, + stunnel4, + uidmap +## End optional utilities Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 860319edf..22b50ce2a 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -201,14 +201,3 @@ systemctl enable vyos-config-cloud-init.service # Update XML cache python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py -# T1797: disable VPP support for rolling release, should be used by developers -# only (in the initial phase). If you wan't to enable VPP use the below command -# on your VyOS installation: -# -# sudo mv /opt/vyatta/share/vyatta-cfg/vpp /opt/vyatta/share/vyatta-cfg/templates/vpp -if [ -d /opt/vyatta/share/vyatta-cfg/templates/vpp ]; then - if [ -d /opt/vyatta/share/vyatta-cfg/vpp ]; then - rm -rf /opt/vyatta/share/vyatta-cfg/vpp - fi - mv /opt/vyatta/share/vyatta-cfg/templates/vpp /opt/vyatta/share/vyatta-cfg/vpp -fi diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst index 9bd6331a8..fbfc85566 100644 --- a/debian/vyos-1x.preinst +++ b/debian/vyos-1x.preinst @@ -5,7 +5,7 @@ dpkg-divert --package vyos-1x --add --no-rename /etc/logrotate.d/conntrackd dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.conf dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.bashrc dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.profile -dpkg-divert --package vyos-1x --add --no-rename /etc/sysctl.d/80-vpp.conf dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplugd.conf dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplug dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.d/45-frr.conf +dpkg-divert --package vyos-1x --add --no-rename /lib/udev/rules.d/99-systemd.rules diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 723223f1c..32c5af9b6 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -38,7 +38,7 @@ <help>Options when using HTTP(S) web request to obtain the IP address</help> </properties> <children> - #include <include/url.xml.i> + #include <include/url-http-https.xml.i> <leafNode name="skip"> <properties> <help>Pattern to skip from the HTTP(S) respose</help> @@ -134,9 +134,9 @@ </tagNode> </children> </tagNode> - <leafNode name="timeout"> + <leafNode name="interval"> <properties> - <help>Time in seconds to wait between DNS updates</help> + <help>Interval in seconds to wait between Dynamic DNS updates</help> <valueHelp> <format>u32:60-3600</format> <description>Time in seconds</description> @@ -144,7 +144,7 @@ <constraint> <validator name="numeric" argument="--range 60-3600"/> </constraint> - <constraintErrorMessage>Timeout must be between 60 and 3600 seconds</constraintErrorMessage> + <constraintErrorMessage>Interval must be between 60 and 3600 seconds</constraintErrorMessage> </properties> <defaultValue>300</defaultValue> </leafNode> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index c4295317a..5ca02acef 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -684,25 +684,8 @@ <defaultValue>1500</defaultValue> </leafNode> #include <include/name-server-ipv4-ipv6-port.xml.i> + #include <include/source-address-ipv4-ipv6-multi.xml.i> <leafNode name="source-address"> - <properties> - <help>Local addresses from which to send DNS queries</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address from which to send traffic</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address from which to send traffic</description> - </valueHelp> - <multi/> - <constraint> - <validator name="ip-address"/> - </constraint> - </properties> <defaultValue>0.0.0.0 ::</defaultValue> </leafNode> <leafNode name="system"> 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/accel-ppp/max-concurrent-sessions.xml.i b/interface-definitions/include/accel-ppp/max-concurrent-sessions.xml.i new file mode 100644 index 000000000..f6ef41019 --- /dev/null +++ b/interface-definitions/include/accel-ppp/max-concurrent-sessions.xml.i @@ -0,0 +1,15 @@ +<!-- include start from accel-ppp/max-concurrent-sessions.xml.i --> +<leafNode name="max-concurrent-sessions"> + <properties> + <help>Maximum number of concurrent session start attempts</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Maximum number of concurrent session start attempts</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 0-65535"/> + </constraint> + <constraintErrorMessage>Maximum concurent sessions must be in range 0-65535</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i b/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i index b8dbe73b2..c0367b891 100644 --- a/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i +++ b/interface-definitions/include/accel-ppp/radius-additions-rate-limit.xml.i @@ -13,12 +13,6 @@ <leafNode name="vendor"> <properties> <help>Vendor dictionary</help> - <completionHelp> - <list>alcatel cisco microsoft mikrotik</list> - </completionHelp> - <constraint> - <validator name="accel-radius-dictionary" /> - </constraint> </properties> </leafNode> <leafNode name="enable"> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 504385b53..4e43298bc 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"> @@ -805,6 +806,76 @@ <valueless/> </properties> </leafNode> + <node name="default-originate"> + <properties> + <help>Originate a default route</help> + </properties> + <children> + <leafNode name="ipv4"> + <properties> + <help>IPv4 address family</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ipv6"> + <properties> + <help>IPv6 address family</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-ead-evi-rx"> + <properties> + <help>Activate PE on EAD-ES even if EAD-EVI is not received</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-ead-evi-tx"> + <properties> + <help>Do not advertise EAD-EVI for local ESs</help> + <valueless/> + </properties> + </leafNode> + <node name="ead-es-frag"> + <properties> + <help>EAD ES fragment config</help> + </properties> + <children> + <leafNode name="evi-limit"> + <properties> + <help>EVIs per-fragment</help> + <valueHelp> + <format>u32:1-1000</format> + <description>limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="ead-es-route-target"> + <properties> + <help>EAD ES Route Target</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Route Target export</help> + <valueHelp> + <format>txt</format> + <description>Route target (A.B.C.D:MN|EF:OPQR|GHJK:MN)</description> + </valueHelp> + <constraint> + <validator name="bgp-rd-rt" argument="--route-target-multi"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> <node name="flooding"> <properties> <help>Specify handling for BUM packets</help> diff --git a/interface-definitions/include/firewall/common-rule-bridge.xml.i b/interface-definitions/include/firewall/common-rule-bridge.xml.i index 381e04b1e..a27cae43b 100644 --- a/interface-definitions/include/firewall/common-rule-bridge.xml.i +++ b/interface-definitions/include/firewall/common-rule-bridge.xml.i @@ -23,25 +23,7 @@ </completionHelp> </properties> </leafNode> -<leafNode name="log"> - <properties> - <help>Option to log packets matching rule</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable log</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable log</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> -</leafNode> +#include <include/firewall/log.xml.i> #include <include/firewall/rule-log-options.xml.i> <node name="source"> <properties> diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i index a55a1a551..aabefcb27 100644 --- a/interface-definitions/include/firewall/common-rule-inet.xml.i +++ b/interface-definitions/include/firewall/common-rule-inet.xml.i @@ -82,44 +82,7 @@ </leafNode> </children> </node> -<leafNode name="log"> - <properties> - <help>Option to log packets matching rule</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable log</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable log</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> -</leafNode> -<leafNode name="log"> - <properties> - <help>Option to log packets matching rule</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable log</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable log</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> -</leafNode> +#include <include/firewall/log.xml.i> #include <include/firewall/rule-log-options.xml.i> <node name="connection-status"> <properties> @@ -222,89 +185,7 @@ </children> </node> #include <include/firewall/synproxy.xml.i> -<node name="state"> - <properties> - <help>Session state</help> - </properties> - <children> - <leafNode name="established"> - <properties> - <help>Established state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="invalid"> - <properties> - <help>Invalid state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="new"> - <properties> - <help>New state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="related"> - <properties> - <help>Related state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - </children> -</node> +#include <include/firewall/state.xml.i> #include <include/firewall/tcp-flags.xml.i> #include <include/firewall/tcp-mss.xml.i> <node name="time"> diff --git a/interface-definitions/include/firewall/inbound-interface-no-group.xml.i b/interface-definitions/include/firewall/inbound-interface-no-group.xml.i new file mode 100644 index 000000000..bcd4c9570 --- /dev/null +++ b/interface-definitions/include/firewall/inbound-interface-no-group.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/inbound-interface-no-group.xml.i --> +<node name="inbound-interface"> + <properties> + <help>Match inbound-interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Match interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <path>vrf name</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <valueHelp> + <format>txt*</format> + <description>Interface name with wildcard</description> + </valueHelp> + <valueHelp> + <format>!txt</format> + <description>Inverted interface name to match</description> + </valueHelp> + <constraint> + <regex>(\!?)(bond|br|dum|en|ersp|eth|gnv|ifb|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)([0-9]?)(\*?)(.+)?|(\!?)lo</regex> + <validator name="vrf-name"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file 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/firewall/log.xml.i b/interface-definitions/include/firewall/log.xml.i index 46d20c1df..21b883e6a 100644 --- a/interface-definitions/include/firewall/log.xml.i +++ b/interface-definitions/include/firewall/log.xml.i @@ -1,15 +1,8 @@ <!-- include start from firewall/log.xml.i --> -<node name="log"> +<leafNode name="log"> <properties> - <help>Option to log packets</help> + <help>Enable log</help> + <valueless/> </properties> - <children> - <leafNode name="enable"> - <properties> - <help>Enable logging</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> +</leafNode> <!-- include end --> diff --git a/interface-definitions/include/firewall/match-interface.xml.i b/interface-definitions/include/firewall/match-interface.xml.i index 1cd9f8c4a..5da6f51fb 100644 --- a/interface-definitions/include/firewall/match-interface.xml.i +++ b/interface-definitions/include/firewall/match-interface.xml.i @@ -1,5 +1,5 @@ <!-- include start from firewall/match-interface.xml.i --> -<leafNode name="interface-name"> +<leafNode name="name"> <properties> <help>Match interface</help> <completionHelp> @@ -24,7 +24,7 @@ </constraint> </properties> </leafNode> -<leafNode name="interface-group"> +<leafNode name="group"> <properties> <help>Match interface-group</help> <completionHelp> diff --git a/interface-definitions/include/firewall/outbound-interface-no-group.xml.i b/interface-definitions/include/firewall/outbound-interface-no-group.xml.i new file mode 100644 index 000000000..e3bace42d --- /dev/null +++ b/interface-definitions/include/firewall/outbound-interface-no-group.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/outbound-interface-no-group.xml.i --> +<node name="outbound-interface"> + <properties> + <help>Match outbound-interface</help> + </properties> + <children> + <leafNode name="name"> + <properties> + <help>Match interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <path>vrf name</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <valueHelp> + <format>txt*</format> + <description>Interface name with wildcard</description> + </valueHelp> + <valueHelp> + <format>!txt</format> + <description>Inverted interface name to match</description> + </valueHelp> + <constraint> + <regex>(\!?)(bond|br|dum|en|ersp|eth|gnv|ifb|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)([0-9]?)(\*?)(.+)?|(\!?)lo</regex> + <validator name="vrf-name"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/state.xml.i b/interface-definitions/include/firewall/state.xml.i new file mode 100644 index 000000000..dee9722e5 --- /dev/null +++ b/interface-definitions/include/firewall/state.xml.i @@ -0,0 +1,30 @@ +<!-- include start from firewall/state.xml.i --> +<leafNode name="state"> + <properties> + <help>Session state</help> + <completionHelp> + <list>established invalid new related</list> + </completionHelp> + <valueHelp> + <format>established</format> + <description>Established state</description> + </valueHelp> + <valueHelp> + <format>invalid</format> + <description>Invalid state</description> + </valueHelp> + <valueHelp> + <format>new</format> + <description>New state</description> + </valueHelp> + <valueHelp> + <format>related</format> + <description>Related state</description> + </valueHelp> + <constraint> + <regex>(established|invalid|new|related)</regex> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/isis/level-1-2-leaf.xml.i b/interface-definitions/include/isis/level-1-2-leaf.xml.i new file mode 100644 index 000000000..3703da1ed --- /dev/null +++ b/interface-definitions/include/isis/level-1-2-leaf.xml.i @@ -0,0 +1,13 @@ +<!-- include start from isis/level-1-2-leaf.xml.i --> +<leafNode name="level-1"> + <properties> + <help>Match on IS-IS level-1 routes</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="level-2"> + <properties> + <help>Match on IS-IS level-2 routes</help> + <valueless/> + </properties> +</leafNode>
\ No newline at end of file diff --git a/interface-definitions/include/isis/lfa-local.xml.i b/interface-definitions/include/isis/lfa-local.xml.i new file mode 100644 index 000000000..c5bf6a3eb --- /dev/null +++ b/interface-definitions/include/isis/lfa-local.xml.i @@ -0,0 +1,128 @@ +<!-- include start from isis/lfa-local.xml.i --> +<node name="local"> + <properties> + <help>Local loop free alternate options</help> + </properties> + <children> + <node name="load-sharing"> + <properties> + <help>Load share prefixes across multiple backups</help> + </properties> + <children> + <node name="disable"> + <properties> + <help>Disable load sharing</help> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </node> + </children> + </node> + <node name="priority-limit"> + <properties> + <help>Limit backup computation up to the prefix priority</help> + </properties> + <children> + <node name="medium"> + <properties> + <help>Compute for critical, high, and medium priority prefixes</help> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </node> + <node name="high"> + <properties> + <help>Compute for critical, and high priority prefixes</help> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </node> + <node name="critical"> + <properties> + <help>Compute for critical priority prefixes only</help> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </node> + </children> + </node> + <node name="tiebreaker"> + <properties> + <help>Configure tiebreaker for multiple backups</help> + </properties> + <children> + <node name="downstream"> + <properties> + <help>Prefer backup path via downstream node</help> + </properties> + <children> + <tagNode name="index"> + <properties> + <help>Set preference order among tiebreakers</help> + <valueHelp> + <format>u32:1-255</format> + <description>The index integer value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="lowest-backup-metric"> + <properties> + <help>Prefer backup path with lowest total metric</help> + </properties> + <children> + <tagNode name="index"> + <properties> + <help>Set preference order among tiebreakers</help> + <valueHelp> + <format>u32:1-255</format> + <description>The index integer value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="node-protecting"> + <properties> + <help>Prefer node protecting backup path</help> + </properties> + <children> + <tagNode name="index"> + <properties> + <help>Set preference order among tiebreakers</help> + <valueHelp> + <format>u32:1-255</format> + <description>The index integer value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/isis/lfa-protocol.xml.i b/interface-definitions/include/isis/lfa-protocol.xml.i new file mode 100644 index 000000000..cfb1a6dc1 --- /dev/null +++ b/interface-definitions/include/isis/lfa-protocol.xml.i @@ -0,0 +1,11 @@ +<!-- include start from isis/lfa-protocol.xml.i --> +<node name="lfa"> + <properties> + <help>Loop free alternate functionality</help> + </properties> + <children> + #include <include/isis/lfa-remote.xml.i> + #include <include/isis/lfa-local.xml.i> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/isis/lfa-remote.xml.i b/interface-definitions/include/isis/lfa-remote.xml.i new file mode 100644 index 000000000..8434e35bf --- /dev/null +++ b/interface-definitions/include/isis/lfa-remote.xml.i @@ -0,0 +1,28 @@ +<!-- include start from isis/lfa-remote.xml.i --> +<node name="remote"> + <properties> + <help>Remote loop free alternate options</help> + </properties> + <children> + <tagNode name="prefix-list"> + <properties> + <help>Filter PQ node router ID based on prefix list</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Name of IPv4/IPv6 prefix-list</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + <constraintErrorMessage>Name of prefix-list can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage> + </properties> + <children> + #include <include/isis/level-1-2-leaf.xml.i> + </children> + </tagNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 648f2b319..404f03cb5 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -165,6 +165,14 @@ </properties> </leafNode> #include <include/isis/ldp-sync-protocol.xml.i> +<node name="fast-reroute"> + <properties> + <help>IS-IS fast reroute configuration</help> + </properties> + <children> + #include <include/isis/lfa-protocol.xml.i> + </children> +</node> <leafNode name="net"> <properties> <help>A Network Entity Title for this process (ISO only)</help> diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index 6234e6195..deb13529d 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -34,12 +34,7 @@ #include <include/firewall/nat-balance.xml.i> </children> </node> - <leafNode name="log"> - <properties> - <help>NAT rule logging</help> - <valueless/> - </properties> - </leafNode> + #include <include/firewall/log.xml.i> <leafNode name="packet-type"> <properties> <help>Packet type</help> diff --git a/interface-definitions/include/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i index 8eab04d4a..4405f9c26 100644 --- a/interface-definitions/include/policy/route-common.xml.i +++ b/interface-definitions/include/policy/route-common.xml.i @@ -77,25 +77,7 @@ </leafNode>
</children>
</node>
-<leafNode name="log">
- <properties>
- <help>Option to log packets matching rule</help>
- <completionHelp>
- <list>enable disable</list>
- </completionHelp>
- <valueHelp>
- <format>enable</format>
- <description>Enable log</description>
- </valueHelp>
- <valueHelp>
- <format>disable</format>
- <description>Disable log</description>
- </valueHelp>
- <constraint>
- <regex>(enable|disable)</regex>
- </constraint>
- </properties>
-</leafNode>
+#include <include/firewall/log.xml.i>
<leafNode name="protocol">
<properties>
<help>Protocol to match (protocol name, number, or "all")</help>
@@ -231,89 +213,7 @@ </leafNode>
</children>
</node>
-<node name="state">
- <properties>
- <help>Session state</help>
- </properties>
- <children>
- <leafNode name="established">
- <properties>
- <help>Established state</help>
- <completionHelp>
- <list>enable disable</list>
- </completionHelp>
- <valueHelp>
- <format>enable</format>
- <description>Enable</description>
- </valueHelp>
- <valueHelp>
- <format>disable</format>
- <description>Disable</description>
- </valueHelp>
- <constraint>
- <regex>(enable|disable)</regex>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="invalid">
- <properties>
- <help>Invalid state</help>
- <completionHelp>
- <list>enable disable</list>
- </completionHelp>
- <valueHelp>
- <format>enable</format>
- <description>Enable</description>
- </valueHelp>
- <valueHelp>
- <format>disable</format>
- <description>Disable</description>
- </valueHelp>
- <constraint>
- <regex>(enable|disable)</regex>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="new">
- <properties>
- <help>New state</help>
- <completionHelp>
- <list>enable disable</list>
- </completionHelp>
- <valueHelp>
- <format>enable</format>
- <description>Enable</description>
- </valueHelp>
- <valueHelp>
- <format>disable</format>
- <description>Disable</description>
- </valueHelp>
- <constraint>
- <regex>(enable|disable)</regex>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="related">
- <properties>
- <help>Related state</help>
- <completionHelp>
- <list>enable disable</list>
- </completionHelp>
- <valueHelp>
- <format>enable</format>
- <description>Enable</description>
- </valueHelp>
- <valueHelp>
- <format>disable</format>
- <description>Disable</description>
- </valueHelp>
- <constraint>
- <regex>(enable|disable)</regex>
- </constraint>
- </properties>
- </leafNode>
- </children>
-</node>
+#include <include/firewall/state.xml.i>
#include <include/firewall/tcp-flags.xml.i>
#include <include/firewall/tcp-mss.xml.i>
<node name="time">
diff --git a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i index a0cdcd7c3..e454b9025 100644 --- a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i +++ b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i @@ -25,27 +25,7 @@ #include <include/radius-server-auth-port.xml.i> </children> </tagNode> - <leafNode name="source-address"> - <properties> - <help>Source IP address used to initiate connection</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>IPv4 source address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 source address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> + #include <include/source-address-ipv4-ipv6-multi.xml.i> <leafNode name="security-mode"> <properties> <help>Security mode for RADIUS authentication</help> diff --git a/interface-definitions/include/source-address-ipv4-ipv6-multi.xml.i b/interface-definitions/include/source-address-ipv4-ipv6-multi.xml.i new file mode 100644 index 000000000..d56ca5be6 --- /dev/null +++ b/interface-definitions/include/source-address-ipv4-ipv6-multi.xml.i @@ -0,0 +1,22 @@ +<!-- include start from source-address-ipv4-ipv6-multi.xml.i --> +<leafNode name="source-address"> + <properties> + <help>Source IP address used to initiate connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/url-http-https.xml.i b/interface-definitions/include/url-http-https.xml.i new file mode 100644 index 000000000..f763c2bb2 --- /dev/null +++ b/interface-definitions/include/url-http-https.xml.i @@ -0,0 +1,15 @@ +<!-- include start from url-http-https.xml.i --> +<leafNode name="url"> + <properties> + <help>Remote URL</help> + <valueHelp> + <format>url</format> + <description>Remote HTTP(S) URL</description> + </valueHelp> + <constraint> + <validator name="url" argument="--scheme http --scheme https"/> + </constraint> + <constraintErrorMessage>Invalid HTTP(S) URL format</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/url.xml.i b/interface-definitions/include/url.xml.i deleted file mode 100644 index caa6f67bd..000000000 --- a/interface-definitions/include/url.xml.i +++ /dev/null @@ -1,15 +0,0 @@ -<!-- include start from url.xml.i --> -<leafNode name="url"> - <properties> - <help>Remote URL</help> - <valueHelp> - <format>url</format> - <description>Remote URL</description> - </valueHelp> - <constraint> - <regex>^https?:\/\/?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\:[0-9]+)*(\/.*)?</regex> - </constraint> - <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> - </properties> -</leafNode> -<!-- include end --> 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/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i index b25fc6e76..7bdb90a35 100644 --- a/interface-definitions/include/version/dns-dynamic-version.xml.i +++ b/interface-definitions/include/version/dns-dynamic-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dns-dynamic-version.xml.i --> -<syntaxVersion component='dns-dynamic' version='1'></syntaxVersion> +<syntaxVersion component='dns-dynamic' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i index dd21bfaca..299eebb00 100644 --- a/interface-definitions/include/version/firewall-version.xml.i +++ b/interface-definitions/include/version/firewall-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/firewall-version.xml.i --> -<syntaxVersion component='firewall' version='11'></syntaxVersion> +<syntaxVersion component='firewall' version='13'></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/nat-version.xml.i b/interface-definitions/include/version/nat-version.xml.i index 027216a07..656da6e14 100644 --- a/interface-definitions/include/version/nat-version.xml.i +++ b/interface-definitions/include/version/nat-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/nat-version.xml.i --> -<syntaxVersion component='nat' version='5'></syntaxVersion> +<syntaxVersion component='nat' version='7'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/nat66-version.xml.i b/interface-definitions/include/version/nat66-version.xml.i index 7b7123dcc..478ca080f 100644 --- a/interface-definitions/include/version/nat66-version.xml.i +++ b/interface-definitions/include/version/nat66-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/nat66-version.xml.i --> -<syntaxVersion component='nat66' version='1'></syntaxVersion> +<syntaxVersion component='nat66' version='2'></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/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i index 2c96e0f15..4fbe757f5 100644 --- a/interface-definitions/include/version/policy-version.xml.i +++ b/interface-definitions/include/version/policy-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/policy-version.xml.i --> -<syntaxVersion component='policy' version='6'></syntaxVersion> +<syntaxVersion component='policy' version='7'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 427e04a54..86c4776b6 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -56,6 +56,60 @@ #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mirror.xml.i> + <node name="evpn"> + <properties> + <help>EVPN Multihoming</help> + </properties> + <children> + <leafNode name="es-df-pref"> + <properties> + <help>Preference value used for designated forwarder (DF) election</help> + <valueHelp> + <format>u32:1-65535</format> + <description>DF Preference value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="es-id"> + <properties> + <help>Ethernet segment identifier</help> + <valueHelp> + <format>u32:1-16777215</format> + <description>Local discriminator</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>10-byte ID - 00:11:22:33:44:55:AA:BB:CC:DD</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + <regex>([0-9A-Fa-f][0-9A-Fa-f]:){9}[0-9A-Fa-f][0-9A-Fa-f]</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="es-sys-mac"> + <properties> + <help>Ethernet segment system MAC</help> + <valueHelp> + <format>macaddr</format> + <description>MAC address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="uplink"> + <properties> + <help>Uplink to the VXLAN core</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> <leafNode name="hash-policy"> <properties> <help>Bonding transmit hash policy</help> 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-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index b78f92c85..30fcb8573 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -109,6 +109,20 @@ <leafNode name="mtu"> <defaultValue>1492</defaultValue> </leafNode> + <leafNode name="mru"> + <properties> + <help>Maximum Receive Unit (MRU)</help> + <valueHelp> + <format>u32:128-16384</format> + <description>Maximum Receive Unit in byte</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + <constraintErrorMessage>MRU must be between 128 and 16384</constraintErrorMessage> + </properties> + <defaultValue>1492</defaultValue> + </leafNode> #include <include/interface/no-peer-dns.xml.i> <leafNode name="remote-address"> <properties> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index b246d9a09..f20743a65 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -19,12 +19,6 @@ #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> - <leafNode name="external"> - <properties> - <help>Use external control plane</help> - <valueless/> - </properties> - </leafNode> <leafNode name="gpe"> <properties> <help>Enable Generic Protocol extension (VXLAN-GPE)</help> @@ -83,17 +77,29 @@ #include <include/interface/parameters-flowlabel.xml.i> </children> </node> + <leafNode name="external"> + <properties> + <help>Use external control plane</help> + <valueless/> + </properties> + </leafNode> <leafNode name="nolearning"> <properties> <help>Do not add unknown addresses into forwarding database</help> <valueless/> </properties> </leafNode> + <leafNode name="neighbor-suppress"> + <properties> + <help>Enable neighbor discovery (ARP and ND) suppression</help> + <valueless/> + </properties> + </leafNode> </children> </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/nat.xml.in b/interface-definitions/nat.xml.in index a06ceefb6..0a639bd80 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -14,7 +14,7 @@ #include <include/nat-rule.xml.i> <tagNode name="rule"> <children> - #include <include/inbound-interface.xml.i> + #include <include/firewall/inbound-interface.xml.i> <node name="translation"> <properties> <help>Inside NAT IP (destination NAT only)</help> @@ -77,7 +77,7 @@ <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - #include <include/nat-interface.xml.i> + #include <include/firewall/outbound-interface.xml.i> <node name="translation"> <properties> <help>Outside NAT IP (source NAT only)</help> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index 7a8970bdf..2fd95e03a 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -32,20 +32,8 @@ </properties> </leafNode> #include <include/nat-exclude.xml.i> - <leafNode name="log"> - <properties> - <help>NAT66 rule logging</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="outbound-interface"> - <properties> - <help>Outbound interface of NAT66 traffic</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - </leafNode> + #include <include/firewall/log.xml.i> + #include <include/firewall/outbound-interface-no-group.xml.i> #include <include/nat/protocol.xml.i> <node name="destination"> <properties> @@ -166,15 +154,7 @@ <valueless/> </properties> </leafNode> - <leafNode name="inbound-interface"> - <properties> - <help>Inbound interface of NAT66 traffic</help> - <completionHelp> - <list>any</list> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - </leafNode> + #include <include/firewall/inbound-interface-no-group.xml.i> #include <include/nat/protocol.xml.i> <node name="destination"> <properties> 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/protocols_static_neighbor-proxy.xml.in b/interface-definitions/protocols_static_neighbor-proxy.xml.in new file mode 100644 index 000000000..1c8433a39 --- /dev/null +++ b/interface-definitions/protocols_static_neighbor-proxy.xml.in @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="static"> + <children> + <node name="neighbor-proxy" owner="${vyos_conf_scripts_dir}/protocols_static_neighbor-proxy.py"> + <properties> + <help>Neighbor proxy parameters</help> + </properties> + <children> + <tagNode name="arp"> + <properties> + <help>IP address for selective ARP proxy</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 destination address allowed for proxy-arp</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + #include <include/generic-interface-multi.xml.i> + </children> + </tagNode> + <tagNode name="nd"> + <properties> + <help>IPv6 address for selective NDP proxy</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 destination address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + #include <include/generic-interface-multi.xml.i> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service-ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index b6e6503d3..9ac0c8fdf 100644 --- a/interface-definitions/service-ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -102,6 +102,7 @@ #include <include/accel-ppp/vlan.xml.i> </children> </tagNode> + #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> <node name="client-ip-pool"> <properties> diff --git a/interface-definitions/service-monitoring-telegraf.xml.in b/interface-definitions/service-monitoring-telegraf.xml.in index ae0bae900..4d694114a 100644 --- a/interface-definitions/service-monitoring-telegraf.xml.in +++ b/interface-definitions/service-monitoring-telegraf.xml.in @@ -53,7 +53,7 @@ </properties> <defaultValue>main</defaultValue> </leafNode> - #include <include/url.xml.i> + #include <include/url-http-https.xml.i> #include <include/port-number.xml.i> <leafNode name="port"> <defaultValue>8086</defaultValue> @@ -145,7 +145,7 @@ <constraintErrorMessage>Table is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> </properties> </leafNode> - #include <include/url.xml.i> + #include <include/url-http-https.xml.i> </children> </node> <leafNode name="source"> @@ -271,7 +271,7 @@ </leafNode> </children> </node> - #include <include/url.xml.i> + #include <include/url-http-https.xml.i> </children> </node> #include <include/interface/vrf.xml.i> diff --git a/interface-definitions/service-pppoe-server.xml.in b/interface-definitions/service-pppoe-server.xml.in index 022ac2885..44b689fe1 100644 --- a/interface-definitions/service-pppoe-server.xml.in +++ b/interface-definitions/service-pppoe-server.xml.in @@ -73,6 +73,7 @@ </children> </tagNode> #include <include/accel-ppp/gateway-address.xml.i> + #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/accel-ppp/mtu-128-16384.xml.i> <node name="limits"> <properties> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index 0851b8389..ec2151b98 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -79,6 +79,101 @@ </properties> </leafNode> #include <include/generic-description.xml.i> + <node name="mib"> + <properties> + <help>Management information base (MIB)</help> + </properties> + <children> + <leafNode name="interface-max"> + <properties> + <help>Sets the maximum number of interfaces included in IF-MIB data collection</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>Sets the maximum number of interfaces included in IF-MIB data collection</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Sets the interface name prefix to include in the IF-MIB data collection</help> + <completionHelp> + <list>br bond dum eth gnv macsec peth sstpc tun veth vti vtun vxlan wg wlan wwan</list> + </completionHelp> + <valueHelp> + <format>br</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>bond</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>dum</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>eth</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>gnv</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>macsec</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>peth</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>sstpc</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>tun</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>veth</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>vti</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>vtun</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>vxlan</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>wg</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>wlan</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <valueHelp> + <format>wwan</format> + <description>Allow prefix for IF-MIB data collection</description> + </valueHelp> + <constraint> + <regex>(br|bond|dum|eth|gnv|macsec|peth|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> <tagNode name="listen-address"> <properties> <help>IP address to listen for incoming SNMP requests</help> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index 30fea91b0..a2f8beead 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -244,21 +244,7 @@ </leafNode> </children> </tagNode> - <leafNode name="source-address"> - <properties> - <help>Source IP used to communicate with TACACS+ server</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>IPv4 source address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/source-address-ipv4.xml.i> <leafNode name="security-mode"> <properties> <help>Security mode for TACACS+ authentication</help> diff --git a/interface-definitions/system-update-check.xml.in b/interface-definitions/system-update-check.xml.in index e4d7041ec..a7d754003 100644 --- a/interface-definitions/system-update-check.xml.in +++ b/interface-definitions/system-update-check.xml.in @@ -14,7 +14,7 @@ <valueless/> </properties> </leafNode> - #include <include/url.xml.i> + #include <include/url-http-https.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index ee0edc3e3..60a1d323b 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -13,6 +13,7 @@ <help>Remote access L2TP VPN</help> </properties> <children> + #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/accel-ppp/mtu-128-16384.xml.i> <leafNode name="outside-address"> <properties> diff --git a/interface-definitions/vpn-pptp.xml.in b/interface-definitions/vpn-pptp.xml.in index 5a8b4a78a..964c4d21e 100644 --- a/interface-definitions/vpn-pptp.xml.in +++ b/interface-definitions/vpn-pptp.xml.in @@ -13,6 +13,7 @@ <help>Remote access PPTP VPN</help> </properties> <children> + #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/accel-ppp/mtu-128-16384.xml.i> <leafNode name="outside-address"> <properties> diff --git a/interface-definitions/vpn-sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index 9e912063f..9c818ba60 100644 --- a/interface-definitions/vpn-sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in @@ -25,6 +25,7 @@ </node> </children> </node> + #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/interface/mtu-68-1500.xml.i> #include <include/accel-ppp/gateway-address.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> diff --git a/interface-definitions/vpp.xml.in b/interface-definitions/vpp.xml.in deleted file mode 100644 index 3f0758c0a..000000000 --- a/interface-definitions/vpp.xml.in +++ /dev/null @@ -1,342 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="vpp" owner="${vyos_conf_scripts_dir}/vpp.py"> - <properties> - <help>Accelerated data-plane</help> - <priority>295</priority> - </properties> - <children> - <node name="cpu"> - <properties> - <help>CPU settings</help> - </properties> - <children> - <leafNode name="corelist-workers"> - <properties> - <help>List of cores worker threads</help> - <valueHelp> - <format><id></format> - <description>CPU core id</description> - </valueHelp> - <valueHelp> - <format><idN>-<idM></format> - <description>CPU core id range (use '-' as delimiter)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--allow-range --range 0-512"/> - </constraint> - <constraintErrorMessage>not a valid CPU core value or range</constraintErrorMessage> - <multi/> - </properties> - </leafNode> - <leafNode name="main-core"> - <properties> - <help>Main core</help> - <valueHelp> - <format>u32:0-512</format> - <description>Assign main thread to specific core</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-512"/> - </constraint> - </properties> - </leafNode> - <leafNode name="skip-cores"> - <properties> - <help>Skip cores</help> - <valueHelp> - <format>u32:0-512</format> - <description>Skip cores</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-512"/> - </constraint> - </properties> - </leafNode> - <leafNode name="workers"> - <properties> - <help>Create worker threads</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Worker threads</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-512"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - <tagNode name="interface"> - <properties> - <help>Interface</help> - <valueHelp> - <format>ethN</format> - <description>Interface name</description> - </valueHelp> - <constraint> - <regex>((eth|lan)[0-9]+|(eno|ens|enp|enx).+)</regex> - </constraint> - <constraintErrorMessage>Invalid interface name</constraintErrorMessage> - </properties> - <children> - <leafNode name="num-rx-desc"> - <properties> - <help>Number of receive ring descriptors</help> - <valueHelp> - <format>u32:256-8192</format> - <description>Number of receive ring descriptors</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 256-8192"/> - </constraint> - </properties> - </leafNode> - <leafNode name="num-tx-desc"> - <properties> - <help>Number of tranceive ring descriptors</help> - <valueHelp> - <format>u32:256-8192</format> - <description>Number of tranceive ring descriptors</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 256-8192"/> - </constraint> - </properties> - </leafNode> - <leafNode name="num-rx-queues"> - <properties> - <help>Number of receive ring descriptors</help> - <valueHelp> - <format>u32:256-8192</format> - <description>Number of receive queues</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 256-8192"/> - </constraint> - </properties> - </leafNode> - <leafNode name="num-tx-queues"> - <properties> - <help>Number of tranceive ring descriptors</help> - <valueHelp> - <format>u32:256-8192</format> - <description>Number of tranceive queues</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 256-8192"/> - </constraint> - </properties> - </leafNode> - <leafNode name='pci'> - <properties> - <help>PCI address allocation</help> - <valueHelp> - <format>auto</format> - <description>Auto detect PCI address</description> - </valueHelp> - <valueHelp> - <format><xxxx:xx:xx.x></format> - <description>Set Peripheral Component Interconnect (PCI) address</description> - </valueHelp> - <constraint> - <regex>(auto|[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])</regex> - </constraint> - </properties> - <defaultValue>auto</defaultValue> - </leafNode> - <leafNode name="rx-mode"> - <properties> - <help>Receive packet processing mode</help> - <completionHelp> - <list>polling interrupt adaptive</list> - </completionHelp> - <valueHelp> - <format>polling</format> - <description>Constantly check for new data</description> - </valueHelp> - <valueHelp> - <format>interrupt</format> - <description>Interrupt mode</description> - </valueHelp> - <valueHelp> - <format>adaptive</format> - <description>Adaptive mode</description> - </valueHelp> - <constraint> - <regex>(polling|interrupt|adaptive)</regex> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - <node name="ip"> - <properties> - <help>IP settings</help> - </properties> - <children> - <leafNode name="heap-size"> - <properties> - <help>IPv4 heap size</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Amount of memory (in Mbytes) dedicated to the destination IP lookup table</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967295"/> - </constraint> - </properties> - <defaultValue>32</defaultValue> - </leafNode> - </children> - </node> - <node name="ip6"> - <properties> - <help>IPv6 settings</help> - </properties> - <children> - <leafNode name="heap-size"> - <properties> - <help>IPv6 heap size</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Amount of memory (in Mbytes) dedicated to the destination IP lookup table</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967295"/> - </constraint> - </properties> - <defaultValue>32</defaultValue> - </leafNode> - <leafNode name="hash-buckets"> - <properties> - <help>IPv6 forwarding table hash buckets</help> - <valueHelp> - <format>u32:1-4294967295</format> - <description>IPv6 forwarding table hash buckets</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967295"/> - </constraint> - </properties> - <defaultValue>65536</defaultValue> - </leafNode> - </children> - </node> - <node name="l2learn"> - <properties> - <help>Level 2 MAC address learning settings</help> - </properties> - <children> - <leafNode name="limit"> - <properties> - <help>Number of MAC addresses in the L2 FIB</help> - <valueHelp> - <format>u32:1-4294967295</format> - <description>Number of concurent entries</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4294967295"/> - </constraint> - </properties> - <defaultValue>4194304</defaultValue> - </leafNode> - </children> - </node> - <node name="logging"> - <properties> - <help>Loggint settings</help> - </properties> - <children> - <leafNode name="default-log-level"> - <properties> - <help>default-log-level</help> - <completionHelp> - <list>alert crit debug disabled emerg err info notice warn</list> - </completionHelp> - <valueHelp> - <format>alert</format> - <description>Alert</description> - </valueHelp> - <valueHelp> - <format>crit</format> - <description>Critical</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>Debug</description> - </valueHelp> - <valueHelp> - <format>disabled</format> - <description>Disabled</description> - </valueHelp> - <valueHelp> - <format>emerg</format> - <description>Emergency</description> - </valueHelp> - <valueHelp> - <format>err</format> - <description>Error</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>Informational</description> - </valueHelp> - <valueHelp> - <format>notice</format> - <description>Notice</description> - </valueHelp> - <valueHelp> - <format>warn</format> - <description>Warning</description> - </valueHelp> - <constraint> - <regex>(alert|crit|debug|disabled|emerg|err|info|notice|warn)</regex> - </constraint> - </properties> - </leafNode> - </children> - </node> - <node name="physmem"> - <properties> - <help>Memory settings</help> - </properties> - <children> - <leafNode name="max-size"> - <properties> - <help>Set memory size for protectable memory allocator (pmalloc) memory space</help> - <valueHelp> - <format><number>m</format> - <description>Megabyte</description> - </valueHelp> - <valueHelp> - <format><number>g</format> - <description>Gigabyte</description> - </valueHelp> - </properties> - </leafNode> - </children> - </node> - <node name="unix"> - <properties> - <help>Unix settings</help> - </properties> - <children> - <leafNode name="poll-sleep-usec"> - <properties> - <help>Add a fixed-sleep between main loop poll</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Number of receive queues</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - <defaultValue>0</defaultValue> - </leafNode> - </children> - </node> - </children> - </node> -</interfaceDefinition> 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/include/bgp/show-bgp-common.xml.i b/op-mode-definitions/include/bgp/show-bgp-common.xml.i index de794a879..d888bc3b0 100644 --- a/op-mode-definitions/include/bgp/show-bgp-common.xml.i +++ b/op-mode-definitions/include/bgp/show-bgp-common.xml.i @@ -107,6 +107,12 @@ #include <include/vni-tagnode.xml.i> </children> </node> + <leafNode name="es-vrf"> + <properties> + <help>Ethernet Segment per VRF</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> <leafNode name="import-rt"> <properties> <help>Show import route target</help> @@ -136,11 +142,17 @@ </leafNode> </children> </tagNode> + <leafNode name="next-hops"> + <properties> + <help>EVPN Nexthops</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> <tagNode name="rd"> <properties> - <help>Show detailed BGP neighbor information</help> + <help>Display information for a route distinguisher</help> <completionHelp> - <list>ASN:NN IPADDRESS:NN</list> + <list>ASN:NN IPADDRESS:NN all</list> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> diff --git a/op-mode-definitions/include/isis-common.xml.i b/op-mode-definitions/include/isis-common.xml.i index e94d868e8..493a56633 100644 --- a/op-mode-definitions/include/isis-common.xml.i +++ b/op-mode-definitions/include/isis-common.xml.i @@ -17,6 +17,33 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </tagNode> +<node name="fast-reroute"> + <properties> + <help>Show IS-IS fast reroute/loop free alternate (lfa) information</help> + </properties> + <children> + <node name="summary"> + <properties> + <help>Show summary of fast reroute/loop free alternate (lfa) information</help> + </properties> + <children> + <leafNode name="level-1"> + <properties> + <help>Show level-1 specific fast reroute/loop free alternate (lfa) information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="level-2"> + <properties> + <help>Show level-2 specific fast reroute/loop free alternate (lfa) information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> +</node> <leafNode name="hostname"> <properties> <help>Show IS-IS dynamic hostname mapping</help> diff --git a/op-mode-definitions/mdns-reflector.xml.in b/op-mode-definitions/mdns-reflector.xml.in new file mode 100644 index 000000000..a90d4d385 --- /dev/null +++ b/op-mode-definitions/mdns-reflector.xml.in @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <children> + <node name="mdns"> + <properties> + <help>Monitor last lines of multicast Domain Name System related services</help> + </properties> + <children> + <node name="repeater"> + <properties> + <help>Monitor last lines of mDNS repeater service</help> + </properties> + <command>journalctl --no-hostname --follow --boot --unit avahi-daemon.service</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="log"> + <children> + <node name="mdns"> + <properties> + <help>Show log for multicast Domain Name System related services</help> + </properties> + <children> + <node name="repeater"> + <properties> + <help>Show log for mDNS repeater service</help> + </properties> + <command>journalctl --no-hostname --boot --unit avahi-daemon.service</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="mdns"> + <properties> + <help>Restart specific multicast Domain Name System service</help> + </properties> + <children> + <node name="repeater"> + <properties> + <help>Restart mDNS repeater service</help> + </properties> + <command>sudo systemctl restart avahi-daemon.service</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 52b5b85d4..44628a112 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -274,12 +274,20 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit snmpd.service</command> </leafNode> - <leafNode name="ssh"> + <node name="ssh"> <properties> <help>Monitor last lines of Secure Shell log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit ssh.service</command> - </leafNode> + <children> + <node name="dynamic-protection"> + <properties> + <help>Monitor last lines of SSH guard log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit sshguard.service</command> + </node> + </children> + </node> <leafNode name="vpn"> <properties> <help>Monitor last lines of ALL Virtual Private Network services</help> @@ -333,12 +341,6 @@ </tagNode> </children> </node> - <leafNode name="vpp"> - <properties> - <help>Monitor last lines of Vector Packet Processor log</help> - </properties> - <command>journalctl --no-hostname --boot --follow --unit vpp.service</command> - </leafNode> <leafNode name="vrrp"> <properties> <help>Monitor last lines of Virtual Router Redundancy Protocol log</help> diff --git a/op-mode-definitions/mtr.xml.in b/op-mode-definitions/mtr.xml.in new file mode 100644 index 000000000..8239aec4c --- /dev/null +++ b/op-mode-definitions/mtr.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <tagNode name="traceroute"> + <properties> + <help>Monitor Traceroute and ping path to target</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/mtr.py ${@:3}</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>mtr options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/mtr.py --get-options-nested "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/mtr.py ${@:3}</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + <tagNode name="mtr"> + <properties> + <help>Monitor Traceroute and ping path to target</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/mtr.py ${@:2}</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>Traceroute options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/mtr.py --get-options "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/mtr.py ${@:2}</command> + </leafNode> + </children> + </tagNode> +</interfaceDefinition> diff --git a/op-mode-definitions/nat66.xml.in b/op-mode-definitions/nat66.xml.in index 6a8a39000..4df20d847 100644 --- a/op-mode-definitions/nat66.xml.in +++ b/op-mode-definitions/nat66.xml.in @@ -16,7 +16,7 @@ <properties> <help>Show configured source NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet6</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet6</command> </node> <node name="statistics"> <properties> @@ -39,7 +39,7 @@ <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6 --address "$6"</command> </tagNode> </children> - <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6</command> </node> </children> </node> @@ -52,7 +52,7 @@ <properties> <help>Show configured destination NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet6</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet6</command> </node> <node name="statistics"> <properties> @@ -75,7 +75,7 @@ <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6 --address "$6"</command> </tagNode> </children> - <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6</command> </node> </children> </node> 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-log.xml.in b/op-mode-definitions/show-log.xml.in index a2a210543..3a622cfb5 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -618,12 +618,20 @@ </properties> <command>journalctl --no-hostname --boot --unit snmpd.service</command> </leafNode> - <leafNode name="ssh"> + <node name="ssh"> <properties> <help>Show log for Secure Shell (SSH)</help> </properties> <command>journalctl --no-hostname --boot --unit ssh.service</command> - </leafNode> + <children> + <node name="dynamic-protection"> + <properties> + <help>Show SSH guard log</help> + </properties> + <command>journalctl --no-hostname --boot --unit sshguard.service</command> + </node> + </children> + </node> <tagNode name="tail"> <properties> <help>Show last n changes to messages</help> @@ -692,12 +700,6 @@ </tagNode> </children> </node> - <leafNode name="vpp"> - <properties> - <help>Show log for Vector Packet Processor (VPP)</help> - </properties> - <command>journalctl --no-hostname --boot --unit vpp.service</command> - </leafNode> <leafNode name="vrrp"> <properties> <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help> diff --git a/op-mode-definitions/show-ssh.xml.in b/op-mode-definitions/show-ssh.xml.in new file mode 100644 index 000000000..ca8e669b3 --- /dev/null +++ b/op-mode-definitions/show-ssh.xml.in @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ssh"> + <properties> + <help>Show SSH server information</help> + </properties> + <children> + <node name="dynamic-protection"> + <properties> + <help>Show SSH server dynamic-protection blocked attackers</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/ssh.py show_dynamic_protection</command> + </node> + <node name="fingerprints"> + <properties> + <help>Show SSH server public key fingerprints</help> + </properties> + <command>${vyos_op_scripts_dir}/ssh.py show_fingerprints</command> + <children> + <node name="ascii"> + <properties> + <help>Show visual ASCII art representation of the public key</help> + </properties> + <command>${vyos_op_scripts_dir}/ssh.py show_fingerprints --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/firewall.py b/python/vyos/firewall.py index c07ed1adf..a2622fa00 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -87,7 +87,6 @@ def nft_action(vyos_action): def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output = [] - #def_suffix = '6' if ip_name == 'ip6' else '' if ip_name == 'ip6': def_suffix = '6' @@ -97,7 +96,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): family = 'bri' if ip_name == 'bri' else 'ipv4' if 'state' in rule_conf and rule_conf['state']: - states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable']) + states = ",".join([s for s in rule_conf['state']]) if states: output.append(f'ct state {{{states}}}') @@ -275,14 +274,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): if 'inbound_interface' in rule_conf: operator = '' - if 'interface_name' in rule_conf['inbound_interface']: - iiface = rule_conf['inbound_interface']['interface_name'] + if 'name' in rule_conf['inbound_interface']: + iiface = rule_conf['inbound_interface']['name'] if iiface[0] == '!': operator = '!=' iiface = iiface[1:] output.append(f'iifname {operator} {{{iiface}}}') else: - iiface = rule_conf['inbound_interface']['interface_group'] + iiface = rule_conf['inbound_interface']['group'] if iiface[0] == '!': operator = '!=' iiface = iiface[1:] @@ -290,14 +289,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): if 'outbound_interface' in rule_conf: operator = '' - if 'interface_name' in rule_conf['outbound_interface']: - oiface = rule_conf['outbound_interface']['interface_name'] + if 'name' in rule_conf['outbound_interface']: + oiface = rule_conf['outbound_interface']['name'] if oiface[0] == '!': operator = '!=' oiface = oiface[1:] output.append(f'oifname {operator} {{{oiface}}}') else: - oiface = rule_conf['outbound_interface']['interface_group'] + oiface = rule_conf['outbound_interface']['group'] if oiface[0] == '!': operator = '!=' oiface = oiface[1:] @@ -395,7 +394,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): if 'priority' in rule_conf['vlan']: output.append(f'vlan pcp {rule_conf["vlan"]["priority"]}') - if 'log' in rule_conf and rule_conf['log'] == 'enable': + if 'log' in rule_conf: action = rule_conf['action'] if 'action' in rule_conf else 'accept' #output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') output.append(f'log prefix "[{family}-{hook}-{fw_name}-{rule_id}-{action[:1].upper()}]"') 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/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 1fe5db7cd..8c5a0220e 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -56,6 +56,10 @@ class VXLANIf(Interface): } _command_set = {**Interface._command_set, **{ + 'neigh_suppress': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': 'bridge link set dev {ifname} neigh_suppress {value} learning off', + }, 'vlan_tunnel': { 'validate': lambda v: assert_list(v, ['on', 'off']), 'shellcmd': 'bridge link set dev {ifname} vlan_tunnel {value}', @@ -68,8 +72,8 @@ class VXLANIf(Interface): # - https://man7.org/linux/man-pages/man8/ip-link.8.html mapping = { 'group' : 'group', - 'external' : 'external', 'gpe' : 'gpe', + 'parameters.external' : 'external', 'parameters.ip.df' : 'df', 'parameters.ip.tos' : 'tos', 'parameters.ip.ttl' : 'ttl', @@ -113,6 +117,19 @@ class VXLANIf(Interface): 'port {port} dev {ifname}' self._cmd(cmd.format(**self.config)) + def set_neigh_suppress(self, state): + """ + Controls whether neigh discovery (arp and nd) proxy and suppression + is enabled on the port. By default this flag is off. + """ + + # Determine current OS Kernel neigh_suppress setting - only adjust when needed + tmp = get_interface_config(self.ifname) + cur_state = 'on' if dict_search(f'linkinfo.info_slave_data.neigh_suppress', tmp) == True else 'off' + new_state = 'on' if state else 'off' + if cur_state != new_state: + self.set_interface('neigh_suppress', state) + def set_vlan_vni_mapping(self, state): """ Controls whether vlan to tunnel mapping is enabled on the port. @@ -163,3 +180,9 @@ class VXLANIf(Interface): # Enable/Disable VLAN tunnel mapping # This is only possible after the interface was assigned to the bridge self.set_vlan_vni_mapping(dict_search('vlan_to_vni', config) != None) + + # Enable/Disable neighbor suppression and learning, there is no need to + # explicitly "disable" it, as VXLAN interface will be recreated if anything + # under "parameters" changes. + if dict_search('parameters.neighbor_suppress', config) != None: + self.set_neigh_suppress('on') diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 4aac103ec..5704f8b64 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -167,11 +167,6 @@ class WireGuardIf(Interface): interface setup code and provide a single point of entry when workin on any interface. """ - # remove no longer associated peers first - if 'peer_remove' in config: - for peer, public_key in config['peer_remove'].items(): - self._cmd(f'wg set {self.ifname} peer {public_key} remove') - tmp_file = NamedTemporaryFile('w') tmp_file.write(config['private_key']) tmp_file.flush() diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 9cbc2b96e..392d38772 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -32,14 +32,34 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): translation_str = '' if 'inbound_interface' in rule_conf: - ifname = rule_conf['inbound_interface'] - if ifname != 'any': - output.append(f'iifname "{ifname}"') + operator = '' + if 'name' in rule_conf['inbound_interface']: + iiface = rule_conf['inbound_interface']['name'] + if iiface[0] == '!': + operator = '!=' + iiface = iiface[1:] + output.append(f'iifname {operator} {{{iiface}}}') + else: + iiface = rule_conf['inbound_interface']['group'] + if iiface[0] == '!': + operator = '!=' + iiface = iiface[1:] + output.append(f'iifname {operator} @I_{iiface}') if 'outbound_interface' in rule_conf: - ifname = rule_conf['outbound_interface'] - if ifname != 'any': - output.append(f'oifname "{ifname}"') + operator = '' + if 'name' in rule_conf['outbound_interface']: + oiface = rule_conf['outbound_interface']['name'] + if oiface[0] == '!': + operator = '!=' + oiface = oiface[1:] + output.append(f'oifname {operator} {{{oiface}}}') + else: + oiface = rule_conf['outbound_interface']['group'] + if oiface[0] == '!': + operator = '!=' + oiface = oiface[1:] + output.append(f'oifname {operator} @I_{oiface}') if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': protocol = rule_conf['protocol'] @@ -150,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/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/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/network.py b/python/vyos/utils/network.py index 9354bd495..5d19f256b 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -197,6 +197,21 @@ def get_all_vrfs(): data[name] = entry return data +def interface_list() -> list: + """ + Get list of interfaces in system + :rtype: list + """ + return Section.interfaces() + + +def vrf_list() -> list: + """ + Get list of VRFs in system + :rtype: list + """ + return list(get_all_vrfs().keys()) + def mac2eui64(mac, prefix=None): """ Convert a MAC address to a EUI64 address or, with prefix provided, a full diff --git a/python/vyos/vpp.py b/python/vyos/vpp.py deleted file mode 100644 index 76e5d29c3..000000000 --- a/python/vyos/vpp.py +++ /dev/null @@ -1,315 +0,0 @@ -# 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/>. - -from functools import wraps -from pathlib import Path -from re import search as re_search, fullmatch as re_fullmatch, MULTILINE as re_M -from subprocess import run -from time import sleep - -from vpp_papi import VPPApiClient -from vpp_papi import VPPIOError, VPPValueError - - -class VPPControl: - """Control VPP network stack - """ - - class _Decorators: - """Decorators for VPPControl - """ - - @classmethod - def api_call(cls, decorated_func): - """Check if API is connected before API call - - Args: - decorated_func: function to decorate - - Raises: - VPPIOError: Connection to API is not established - """ - - @wraps(decorated_func) - def api_safe_wrapper(cls, *args, **kwargs): - if not cls.vpp_api_client.transport.connected: - raise VPPIOError(2, 'VPP API is not connected') - return decorated_func(cls, *args, **kwargs) - - return api_safe_wrapper - - @classmethod - def check_retval(cls, decorated_func): - """Check retval from API response - - Args: - decorated_func: function to decorate - - Raises: - VPPValueError: raised when retval is not 0 - """ - - @wraps(decorated_func) - def check_retval_wrapper(cls, *args, **kwargs): - return_value = decorated_func(cls, *args, **kwargs) - if not return_value.retval == 0: - raise VPPValueError( - f'VPP API call failed: {return_value.retval}') - return return_value - - return check_retval_wrapper - - def __init__(self, attempts: int = 5, interval: int = 1000) -> None: - """Create VPP API connection - - Args: - attempts (int, optional): attempts to connect. Defaults to 5. - interval (int, optional): interval between attempts in ms. Defaults to 1000. - - Raises: - VPPIOError: Connection to API cannot be established - """ - self.vpp_api_client = VPPApiClient() - # connect with interval - while attempts: - try: - attempts -= 1 - self.vpp_api_client.connect('vpp-vyos') - break - except (ConnectionRefusedError, FileNotFoundError) as err: - print(f'VPP API connection timeout: {err}') - sleep(interval / 1000) - # raise exception if connection was not successful in the end - if not self.vpp_api_client.transport.connected: - raise VPPIOError(2, 'Cannot connect to VPP API') - - def __del__(self) -> None: - """Disconnect from VPP API (destructor) - """ - self.disconnect() - - def disconnect(self) -> None: - """Disconnect from VPP API - """ - if self.vpp_api_client.transport.connected: - self.vpp_api_client.disconnect() - - @_Decorators.check_retval - @_Decorators.api_call - def cli_cmd(self, command: str): - """Send raw CLI command - - Args: - command (str): command to send - - Returns: - vpp_papi.vpp_serializer.cli_inband_reply: CLI reply class - """ - return self.vpp_api_client.api.cli_inband(cmd=command) - - @_Decorators.api_call - def get_mac(self, ifname: str) -> str: - """Find MAC address by interface name in VPP - - Args: - ifname (str): interface name inside VPP - - Returns: - str: MAC address - """ - for iface in self.vpp_api_client.api.sw_interface_dump(): - if iface.interface_name == ifname: - return iface.l2_address.mac_string - return '' - - @_Decorators.api_call - def get_sw_if_index(self, ifname: str) -> int | None: - """Find interface index by interface name in VPP - - Args: - ifname (str): interface name inside VPP - - Returns: - int | None: Interface index or None (if was not fount) - """ - for iface in self.vpp_api_client.api.sw_interface_dump(): - if iface.interface_name == ifname: - return iface.sw_if_index - return None - - @_Decorators.check_retval - @_Decorators.api_call - def lcp_pair_add(self, iface_name_vpp: str, iface_name_kernel: str) -> None: - """Create LCP interface pair between VPP and kernel - - Args: - iface_name_vpp (str): interface name in VPP - iface_name_kernel (str): interface name in kernel - """ - iface_index = self.get_sw_if_index(iface_name_vpp) - if iface_index: - return self.vpp_api_client.api.lcp_itf_pair_add_del( - is_add=True, - sw_if_index=iface_index, - host_if_name=iface_name_kernel) - - @_Decorators.check_retval - @_Decorators.api_call - def lcp_pair_del(self, iface_name_vpp: str, iface_name_kernel: str) -> None: - """Delete LCP interface pair between VPP and kernel - - Args: - iface_name_vpp (str): interface name in VPP - iface_name_kernel (str): interface name in kernel - """ - iface_index = self.get_sw_if_index(iface_name_vpp) - if iface_index: - return self.vpp_api_client.api.lcp_itf_pair_add_del( - is_add=False, - sw_if_index=iface_index, - host_if_name=iface_name_kernel) - - @_Decorators.check_retval - @_Decorators.api_call - def iface_rxmode(self, iface_name: str, rx_mode: str) -> None: - """Set interface rx-mode in VPP - - Args: - iface_name (str): interface name in VPP - rx_mode (str): mode (polling, interrupt, adaptive) - """ - modes_dict: dict[str, int] = { - 'polling': 1, - 'interrupt': 2, - 'adaptive': 3 - } - if rx_mode not in modes_dict: - raise VPPValueError(f'Mode {rx_mode} is not known') - iface_index = self.get_sw_if_index(iface_name) - return self.vpp_api_client.api.sw_interface_set_rx_mode( - sw_if_index=iface_index, mode=modes_dict[rx_mode]) - - @_Decorators.api_call - def get_pci_addr(self, ifname: str) -> str: - """Find PCI address of interface by interface name in VPP - - Args: - ifname (str): interface name inside VPP - - Returns: - str: PCI address - """ - hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply - - regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$' - re_obj = re_search(regex_filter, hw_info, re_M) - - # return empty string if no interface or no PCI info was found - if not hw_info or not re_obj: - return '' - - address = re_obj.groupdict().get('address', '') - - # we need to modify address to math kernel style - # for example: 0000:06:14.00 -> 0000:06:14.0 - address_chunks: list[str] = address.split('.') - address_normalized: str = f'{address_chunks[0]}.{int(address_chunks[1])}' - - return address_normalized - - -class HostControl: - """Control Linux host - """ - - @staticmethod - def pci_rescan(pci_addr: str = '') -> None: - """Rescan PCI device by removing it and rescan PCI bus - - If PCI address is not defined - just rescan PCI bus - - Args: - address (str, optional): PCI address of device. Defaults to ''. - """ - if pci_addr: - device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove') - if device_file.exists(): - device_file.write_text('1') - # wait 10 seconds max until device will be removed - attempts = 100 - while device_file.exists() and attempts: - attempts -= 1 - sleep(0.1) - if device_file.exists(): - raise TimeoutError( - f'Timeout was reached for removing PCI device {pci_addr}' - ) - else: - raise FileNotFoundError(f'PCI device {pci_addr} does not exist') - rescan_file = Path('/sys/bus/pci/rescan') - rescan_file.write_text('1') - if pci_addr: - # wait 10 seconds max until device will be installed - attempts = 100 - while not device_file.exists() and attempts: - attempts -= 1 - sleep(0.1) - if not device_file.exists(): - raise TimeoutError( - f'Timeout was reached for installing PCI device {pci_addr}') - - @staticmethod - def get_eth_name(pci_addr: str) -> str: - """Find Ethernet interface name by PCI address - - Args: - pci_addr (str): PCI address - - Raises: - FileNotFoundError: no Ethernet interface was found - - Returns: - str: Ethernet interface name - """ - # find all PCI devices with eth* names - net_devs: dict[str, str] = {} - net_devs_dir = Path('/sys/class/net') - regex_filter = r'^/sys/devices/pci[\w/:\.]+/(?P<pci_addr>\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?P<iface_name>eth\d+)$' - for dir in net_devs_dir.iterdir(): - real_dir: str = dir.resolve().as_posix() - re_obj = re_fullmatch(regex_filter, real_dir) - if re_obj: - iface_name: str = re_obj.group('iface_name') - iface_addr: str = re_obj.group('pci_addr') - net_devs.update({iface_addr: iface_name}) - # match to provided PCI address and return a name if found - if pci_addr in net_devs: - return net_devs[pci_addr] - # raise error if device was not found - raise FileNotFoundError( - f'PCI device {pci_addr} not found in ethernet interfaces') - - @staticmethod - def rename_iface(name_old: str, name_new: str) -> None: - """Rename interface - - Args: - name_old (str): old name - name_new (str): new name - """ - rename_cmd: list[str] = [ - 'ip', 'link', 'set', name_old, 'name', name_new - ] - run(rename_cmd) 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/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index 37baee0fd..e10adbbc6 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -141,65 +141,65 @@ set interfaces wireguard wg1 peer sam preshared-key 'XpFtzx2Z+nR8pBv9/sSf7I94OkZ set interfaces wireguard wg1 peer sam public-key 'v5zfKGvH6W/lfDXJ0en96lvKo1gfFxMUWxe02+Fj5BU=' set interfaces wireguard wg1 port '7778' set nat destination rule 50 destination port '49371' -set nat destination rule 50 inbound-interface 'pppoe0' +set nat destination rule 50 inbound-interface name 'pppoe0' set nat destination rule 50 protocol 'tcp_udp' set nat destination rule 50 translation address '192.168.0.5' set nat destination rule 51 destination port '58050-58051' -set nat destination rule 51 inbound-interface 'pppoe0' +set nat destination rule 51 inbound-interface name 'pppoe0' set nat destination rule 51 protocol 'tcp' set nat destination rule 51 translation address '192.168.0.5' set nat destination rule 52 destination port '22067-22070' -set nat destination rule 52 inbound-interface 'pppoe0' +set nat destination rule 52 inbound-interface name 'pppoe0' set nat destination rule 52 protocol 'tcp' set nat destination rule 52 translation address '192.168.0.5' set nat destination rule 53 destination port '34342' -set nat destination rule 53 inbound-interface 'pppoe0' +set nat destination rule 53 inbound-interface name 'pppoe0' set nat destination rule 53 protocol 'tcp_udp' set nat destination rule 53 translation address '192.168.0.121' set nat destination rule 54 destination port '45459' -set nat destination rule 54 inbound-interface 'pppoe0' +set nat destination rule 54 inbound-interface name 'pppoe0' set nat destination rule 54 protocol 'tcp_udp' set nat destination rule 54 translation address '192.168.0.120' set nat destination rule 55 destination port '22' -set nat destination rule 55 inbound-interface 'pppoe0' +set nat destination rule 55 inbound-interface name 'pppoe0' set nat destination rule 55 protocol 'tcp' set nat destination rule 55 translation address '192.168.0.5' set nat destination rule 56 destination port '8920' -set nat destination rule 56 inbound-interface 'pppoe0' +set nat destination rule 56 inbound-interface name 'pppoe0' set nat destination rule 56 protocol 'tcp' set nat destination rule 56 translation address '192.168.0.5' set nat destination rule 60 destination port '80,443' -set nat destination rule 60 inbound-interface 'pppoe0' +set nat destination rule 60 inbound-interface name 'pppoe0' set nat destination rule 60 protocol 'tcp' set nat destination rule 60 translation address '192.168.0.5' set nat destination rule 70 destination port '5001' -set nat destination rule 70 inbound-interface 'pppoe0' +set nat destination rule 70 inbound-interface name 'pppoe0' set nat destination rule 70 protocol 'tcp' set nat destination rule 70 translation address '192.168.0.5' set nat destination rule 80 destination port '25' -set nat destination rule 80 inbound-interface 'pppoe0' +set nat destination rule 80 inbound-interface name 'pppoe0' set nat destination rule 80 protocol 'tcp' set nat destination rule 80 translation address '192.168.0.5' set nat destination rule 90 destination port '8123' -set nat destination rule 90 inbound-interface 'pppoe0' +set nat destination rule 90 inbound-interface name 'pppoe0' set nat destination rule 90 protocol 'tcp' set nat destination rule 90 translation address '192.168.0.7' set nat destination rule 91 destination port '1880' -set nat destination rule 91 inbound-interface 'pppoe0' +set nat destination rule 91 inbound-interface name 'pppoe0' set nat destination rule 91 protocol 'tcp' set nat destination rule 91 translation address '192.168.0.7' set nat destination rule 500 destination address '!192.168.0.0/24' set nat destination rule 500 destination port '53' -set nat destination rule 500 inbound-interface 'eth1' +set nat destination rule 500 inbound-interface name 'eth1' set nat destination rule 500 protocol 'tcp_udp' set nat destination rule 500 source address '!192.168.0.1-192.168.0.5' set nat destination rule 500 translation address '192.168.0.1' -set nat source rule 1000 outbound-interface 'pppoe0' +set nat source rule 1000 outbound-interface name 'pppoe0' set nat source rule 1000 translation address 'masquerade' -set nat source rule 2000 outbound-interface 'vtun0' +set nat source rule 2000 outbound-interface name 'vtun0' set nat source rule 2000 source address '192.168.0.0/16' set nat source rule 2000 translation address 'masquerade' -set nat source rule 3000 outbound-interface 'vtun1' +set nat source rule 3000 outbound-interface name 'vtun1' set nat source rule 3000 translation address 'masquerade' set policy prefix-list user1-routes rule 1 action 'permit' set policy prefix-list user1-routes rule 1 prefix '192.168.0.0/24' 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..8c3e00a2a 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -148,7 +148,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) - self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'interface-group', '!smoketest_interface']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface']) self.cli_commit() @@ -213,24 +213,25 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '172.16.10.10']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log-options', 'level', 'debug']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'ttl', 'eq', '15']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'destination', 'port', '8888']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log-options', 'level', 'err']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'not', 'ack']) 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']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'limit', 'rate', '5/minute']) - self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log', 'disable']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'destination', 'port', '22']) @@ -243,15 +244,16 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast']) - self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'interface-name', interface_wc]) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'name', interface_wc]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return']) 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]) + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'name', interface_inv]) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark]) @@ -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') @@ -295,7 +300,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '1024']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '17']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '52']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'group', '66']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'snapshot-length', '6666']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'queue-threshold','32000']) @@ -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') @@ -407,24 +414,26 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'source', 'address', '2002::1']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '2002::1:1']) - self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log']) 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', 'forward', 'filter', 'rule', '2', 'inbound-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]) + self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'outbound-interface', 'name', interface]) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'udp']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'source', 'address', '2002::1:2']) - self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'interface-name', interface]) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'name', interface]) self.cli_commit() @@ -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') @@ -525,19 +537,19 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established', 'enable']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'accept']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'connection-status', 'nat', 'destination']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'action', 'accept']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new', 'enable']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'connection-status', 'nat', 'source']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'action', 'accept']) - self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'state', 'related', 'enable']) + self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'state', 'related']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'ftp']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '5', 'conntrack-helper', 'pptp']) @@ -569,8 +581,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'bridge', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'source', 'mac-address', mac_address]) - self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'interface-name', interface_in]) - self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'name', interface_in]) + self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log-options', 'level', 'crit']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-action', 'drop']) @@ -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']) @@ -648,14 +698,14 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'offload']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp']) - self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'established', 'enable']) - self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'related', 'enable']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'established']) + self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'state', 'related']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'action', 'offload']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'offload-target', 'smoketest']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp']) - self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'established', 'enable']) - self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'related', 'enable']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'established']) + self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '1', 'state', 'related']) self.cli_commit() diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 8867cb427..419de774a 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -241,5 +241,45 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): for member in self._members: self.assertIn(member, slaves) + def test_bonding_evpn_multihoming(self): + id = '5' + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_set(self._base_path + [interface, 'evpn', 'es-id', id]) + self.cli_set(self._base_path + [interface, 'evpn', 'es-df-pref', id]) + self.cli_set(self._base_path + [interface, 'evpn', 'es-sys-mac', f'00:12:34:56:78:0{id}']) + self.cli_set(self._base_path + [interface, 'evpn', 'uplink']) + + id = int(id) + 1 + + self.cli_commit() + + id = '5' + for interface in self._interfaces: + frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') + + self.assertIn(f' evpn mh es-id {id}', frrconfig) + self.assertIn(f' evpn mh es-df-pref {id}', frrconfig) + self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig) + self.assertIn(f' evpn mh uplink', frrconfig) + + id = int(id) + 1 + + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'evpn', 'es-id']) + self.cli_delete(self._base_path + [interface, 'evpn', 'es-df-pref']) + + self.cli_commit() + + id = '5' + for interface in self._interfaces: + frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') + self.assertIn(f' evpn mh es-sys-mac 00:12:34:56:78:0{id}', frrconfig) + self.assertIn(f' evpn mh uplink', frrconfig) + + id = int(id) + 1 + if __name__ == '__main__': unittest.main(verbosity=2) 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_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 0ce5e2fe0..7b702759f 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -59,10 +59,12 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): user = f'VyOS-user-{interface}' passwd = f'VyOS-passwd-{interface}' mtu = '1400' + mru = '1300' self.cli_set(base_path + [interface, 'authentication', 'username', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'mtu', mtu]) + self.cli_set(base_path + [interface, 'mru', '9000']) self.cli_set(base_path + [interface, 'no-peer-dns']) # check validate() - a source-interface is required @@ -70,6 +72,11 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) + # check validate() - MRU needs to be less or equal then MTU + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + [interface, 'mru', mru]) + # commit changes self.cli_commit() @@ -80,6 +87,8 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) + tmp = get_config_value(interface, 'mru')[1] + self.assertEqual(tmp, mru) tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index e9c9e68fd..17e4fc36f 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -107,7 +107,7 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): def test_vxlan_external(self): interface = 'vxlan0' source_address = '192.0.2.1' - self.cli_set(self._base_path + [interface, 'external']) + self.cli_set(self._base_path + [interface, 'parameters', 'external']) self.cli_set(self._base_path + [interface, 'source-address', source_address]) # Both 'VNI' and 'external' can not be specified at the same time. @@ -150,7 +150,7 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): '31': '10031', } - self.cli_set(self._base_path + [interface, 'external']) + self.cli_set(self._base_path + [interface, 'parameters', 'external']) self.cli_set(self._base_path + [interface, 'source-interface', source_interface]) for vlan, vni in vlan_to_vni.items(): @@ -177,11 +177,50 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): tmp = get_interface_config(interface) self.assertEqual(tmp['master'], bridge) + self.assertFalse(tmp['linkinfo']['info_slave_data']['neigh_suppress']) tmp = get_vxlan_vlan_tunnels('vxlan0') self.assertEqual(tmp, list(vlan_to_vni)) self.cli_delete(['interfaces', 'bridge', bridge]) + def test_vxlan_neighbor_suppress(self): + bridge = 'br555' + interface = 'vxlan555' + source_interface = 'eth0' + + self.cli_set(self._base_path + [interface, 'parameters', 'external']) + self.cli_set(self._base_path + [interface, 'source-interface', source_interface]) + + self.cli_set(self._base_path + [interface, 'parameters', 'neighbor-suppress']) + + # This must fail as this VXLAN interface is not associated with any bridge + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', interface]) + + # commit configuration + self.cli_commit() + + self.assertTrue(interface_exists(bridge)) + self.assertTrue(interface_exists(interface)) + + tmp = get_interface_config(interface) + self.assertEqual(tmp['master'], bridge) + self.assertTrue(tmp['linkinfo']['info_slave_data']['neigh_suppress']) + self.assertFalse(tmp['linkinfo']['info_slave_data']['learning']) + + # Remove neighbor suppress configuration and re-test + self.cli_delete(self._base_path + [interface, 'parameters', 'neighbor-suppress']) + # commit configuration + self.cli_commit() + + tmp = get_interface_config(interface) + self.assertEqual(tmp['master'], bridge) + self.assertFalse(tmp['linkinfo']['info_slave_data']['neigh_suppress']) + self.assertTrue(tmp['linkinfo']['info_slave_data']['learning']) + + self.cli_delete(['interfaces', 'bridge', bridge]) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index 48c7cb6a1..4b994a659 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -20,6 +20,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.file import read_file +from vyos.utils.process import cmd base_path = ['interfaces', 'wireguard'] @@ -152,5 +153,52 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): tmp = read_file(f'/sys/class/net/{interface}/threaded') self.assertTrue(tmp, "1") + def test_05_wireguard_peer_pubkey_change(self): + # T5707 changing WireGuard CLI public key of a peer - it's not removed + + def get_peers(interface) -> list: + tmp = cmd(f'sudo wg show {interface} dump') + first_line = True + peers = [] + for line in tmp.split('\n'): + if not line: + continue # Skip empty lines and last line + items = line.split('\t') + if first_line: + self.assertEqual(privkey, items[0]) + first_line = False + else: + peers.append(items[0]) + return peers + + + interface = 'wg1337' + port = '1337' + privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU=' + pubkey_1 = 'srQ8VF6z/LDjKCzpxBzFpmaNUOeuHYzIfc2dcmoc/h4=' + pubkey_2 = '8pbMHiQ7NECVP7F65Mb2W8+4ldGG2oaGvDSpSEsOBn8=' + + self.cli_set(base_path + [interface, 'address', '172.16.0.1/24']) + self.cli_set(base_path + [interface, 'port', port]) + self.cli_set(base_path + [interface, 'private-key', privkey]) + + self.cli_set(base_path + [interface, 'peer', 'VyOS', 'public-key', pubkey_1]) + self.cli_set(base_path + [interface, 'peer', 'VyOS', 'allowed-ips', '10.205.212.10/32']) + + self.cli_commit() + + peers = get_peers(interface) + self.assertIn(pubkey_1, peers) + self.assertNotIn(pubkey_2, peers) + + # Now change the public key of our peer + self.cli_set(base_path + [interface, 'peer', 'VyOS', 'public-key', pubkey_2]) + self.cli_commit() + + # Verify config + peers = get_peers(interface) + self.assertNotIn(pubkey_1, peers) + self.assertIn(pubkey_2, peers) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 703e5ab28..682fc141d 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -82,12 +82,12 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): # or configured destination address for NAT if int(rule) < 200: self.cli_set(src_path + ['rule', rule, 'source', 'address', network]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_100]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', outbound_iface_100]) self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) nftables_search.append([f'saddr {network}', f'oifname "{outbound_iface_100}"', 'masquerade']) else: self.cli_set(src_path + ['rule', rule, 'destination', 'address', network]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_200]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', outbound_iface_200]) self.cli_set(src_path + ['rule', rule, 'exclude']) nftables_search.append([f'daddr {network}', f'oifname "{outbound_iface_200}"', 'return']) @@ -98,13 +98,15 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): def test_snat_groups(self): address_group = 'smoketest_addr' address_group_member = '192.0.2.1' + interface_group = 'smoketest_ifaces' + interface_group_member = 'bond.99' rule = '100' - outbound_iface = 'eth0' self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) + self.cli_set(['firewall', 'group', 'interface-group', interface_group, 'interface', interface_group_member]) self.cli_set(src_path + ['rule', rule, 'source', 'group', 'address-group', address_group]) - self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'group', interface_group]) self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) self.cli_commit() @@ -112,7 +114,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): nftables_search = [ [f'set A_{address_group}'], [f'elements = {{ {address_group_member} }}'], - [f'ip saddr @A_{address_group}', f'oifname "{outbound_iface}"', 'masquerade'] + [f'ip saddr @A_{address_group}', f'oifname @I_{interface_group}', 'masquerade'] ] self.verify_nftables(nftables_search, 'ip vyos_nat') @@ -136,12 +138,12 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): rule_search = [f'dnat to 192.0.2.1:{port}'] if int(rule) < 200: self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_100]) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_100]) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', inbound_iface_100]) rule_search.append(f'{inbound_proto_100} sport {port}') rule_search.append(f'iifname "{inbound_iface_100}"') else: self.cli_set(dst_path + ['rule', rule, 'protocol', inbound_proto_200]) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_200]) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', inbound_iface_200]) rule_search.append(f'iifname "{inbound_iface_200}"') nftables_search.append(rule_search) @@ -167,7 +169,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): rule = '1000' self.cli_set(dst_path + ['rule', rule, 'destination', 'address', '!192.0.2.1']) self.cli_set(dst_path + ['rule', rule, 'destination', 'port', '53']) - self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'eth0']) + self.cli_set(dst_path + ['rule', rule, 'inbound-interface', 'name', 'eth0']) self.cli_set(dst_path + ['rule', rule, 'protocol', 'tcp_udp']) self.cli_set(dst_path + ['rule', rule, 'source', 'address', '!192.0.2.1']) self.cli_set(dst_path + ['rule', rule, 'translation', 'address', '192.0.2.1']) @@ -186,7 +188,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_dnat_without_translation_address(self): - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443']) self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) self.cli_set(dst_path + ['rule', '1', 'packet-type', 'host']) @@ -236,13 +238,13 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_set(dst_path + ['rule', '10', 'destination', 'address', dst_addr_1]) self.cli_set(dst_path + ['rule', '10', 'destination', 'port', dest_port]) self.cli_set(dst_path + ['rule', '10', 'protocol', protocol]) - self.cli_set(dst_path + ['rule', '10', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '10', 'inbound-interface', 'name', ifname]) self.cli_set(dst_path + ['rule', '10', 'translation', 'redirect', 'port', redirected_port]) self.cli_set(dst_path + ['rule', '20', 'destination', 'address', dst_addr_1]) self.cli_set(dst_path + ['rule', '20', 'destination', 'port', dest_port]) self.cli_set(dst_path + ['rule', '20', 'protocol', protocol]) - self.cli_set(dst_path + ['rule', '20', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '20', 'inbound-interface', 'name', ifname]) self.cli_set(dst_path + ['rule', '20', 'translation', 'redirect']) self.cli_commit() @@ -266,7 +268,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): weight_4 = '65' dst_port = '443' - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', ifname]) self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dst_port]) self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-address']) @@ -276,7 +278,7 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_1, 'weight', weight_1]) self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_2, 'weight', weight_2]) - self.cli_set(src_path + ['rule', '1', 'outbound-interface', ifname]) + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', ifname]) self.cli_set(src_path + ['rule', '1', 'load-balance', 'hash', 'random']) self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_3, 'weight', weight_3]) self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_4, 'weight', weight_4]) diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index e062f28a6..0607f6616 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -56,15 +56,15 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): def test_source_nat66(self): source_prefix = 'fc00::/64' translation_prefix = 'fc01::/64' - self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1']) self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix]) - self.cli_set(src_path + ['rule', '2', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '2', 'outbound-interface', 'name', 'eth1']) self.cli_set(src_path + ['rule', '2', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '2', 'translation', 'address', 'masquerade']) - self.cli_set(src_path + ['rule', '3', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '3', 'outbound-interface', 'name', 'eth1']) self.cli_set(src_path + ['rule', '3', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '3', 'exclude']) @@ -81,7 +81,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): def test_source_nat66_address(self): source_prefix = 'fc00::/64' translation_address = 'fc00::1' - self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1']) self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_address]) @@ -98,11 +98,11 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): destination_address = 'fc00::1' translation_address = 'fc01::1' source_address = 'fc02::1' - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'address', destination_address]) self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address]) - self.cli_set(dst_path + ['rule', '2', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '2', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '2', 'destination', 'address', destination_address]) self.cli_set(dst_path + ['rule', '2', 'source', 'address', source_address]) self.cli_set(dst_path + ['rule', '2', 'exclude']) @@ -124,7 +124,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): sport = '8080' tport = '5555' proto = 'tcp' - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dport]) self.cli_set(dst_path + ['rule', '1', 'source', 'address', source_prefix]) self.cli_set(dst_path + ['rule', '1', 'source', 'port', sport]) @@ -144,7 +144,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): def test_destination_nat66_prefix(self): destination_prefix = 'fc00::/64' translation_prefix = 'fc01::/64' - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'address', destination_prefix]) self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_prefix]) @@ -158,7 +158,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_nat') def test_destination_nat66_without_translation_address(self): - self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'name', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '443']) self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp']) self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '443']) @@ -180,7 +180,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): # check validate() - outbound-interface must be defined with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'eth0']) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', 'name', 'eth0']) # check validate() - translation address not specified with self.assertRaises(ConfigSessionError): @@ -196,7 +196,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): sport = '8080' tport = '80' proto = 'tcp' - self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth1']) self.cli_set(src_path + ['rule', '1', 'destination', 'port', dport]) self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '1', 'source', 'port', sport]) 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_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 72192fb98..32e86ea7e 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -200,7 +200,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'source', 'address', '198.51.100.0/24']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'protocol', 'tcp']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'destination', 'port', '22']) - self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'state', 'new', 'enable']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'state', 'new']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'ttl', 'gt', '2']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'mark', '!456']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '3', 'set', 'table', table_id]) @@ -209,7 +209,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-length', '128']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-length', '1024-2048']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'packet-type', 'other']) - self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'log', 'enable']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'log']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '4', 'set', 'table', table_id]) self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'dscp', '41']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '5', 'dscp', '57-59']) @@ -225,7 +225,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'source', 'address', '2001:db8::0/64']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'protocol', 'tcp']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'destination', 'port', '22']) - self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'state', 'new', 'enable']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'state', 'new']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'hop-limit', 'gt', '2']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '3', 'set', 'table', table_id]) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'protocol', 'icmpv6']) @@ -234,7 +234,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'packet-length-exclude', '1024-2048']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'packet-type', 'multicast']) - self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'log', 'enable']) + self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'log']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '4', 'set', 'table', table_id]) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'dscp-exclude', '61']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'dscp-exclude', '14-19']) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 967958cab..23e138ebe 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) @@ -722,15 +728,25 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): def test_bgp_07_l2vpn_evpn(self): vnis = ['10010', '10020', '10030'] neighbors = ['192.0.2.10', '192.0.2.20', '192.0.2.30'] + evi_limit = '1000' + route_targets = ['1.1.1.1:100', '1.1.1.1:200', '1.1.1.1:300'] self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-all-vni']) self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-default-gw']) self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'advertise-svi-ip']) self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'flooding', 'disable']) + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'default-originate', 'ipv4']) + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'default-originate', 'ipv6']) + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-rx']) + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'disable-ead-evi-tx']) for vni in vnis: self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-default-gw']) self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'vni', vni, 'advertise-svi-ip']) + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'ead-es-frag', 'evi-limit', evi_limit]) + for route_target in route_targets: + self.cli_set(base_path + ['address-family', 'l2vpn-evpn', 'ead-es-route-target', 'export', route_target]) + # commit changes self.cli_commit() @@ -741,12 +757,20 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' advertise-all-vni', frrconfig) self.assertIn(f' advertise-default-gw', frrconfig) self.assertIn(f' advertise-svi-ip', frrconfig) + self.assertIn(f' default-originate ipv4', frrconfig) + self.assertIn(f' default-originate ipv6', frrconfig) + self.assertIn(f' disable-ead-evi-rx', frrconfig) + self.assertIn(f' disable-ead-evi-tx', frrconfig) self.assertIn(f' flooding disable', frrconfig) for vni in vnis: vniconfig = self.getFRRconfig(f' vni {vni}') self.assertIn(f'vni {vni}', vniconfig) self.assertIn(f' advertise-default-gw', vniconfig) self.assertIn(f' advertise-svi-ip', vniconfig) + self.assertIn(f' ead-es-frag evi-limit {evi_limit}', frrconfig) + for route_target in route_targets: + self.assertIn(f' ead-es-route-target export {route_target}', frrconfig) + def test_bgp_09_distance_and_flowspec(self): distance_external = '25' diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 747fb5e80..8b423dbea 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -324,5 +324,65 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.assertIn(f' ipv6 router isis {domain}', tmp) self.assertIn(f' no isis mpls ldp-sync', tmp) + def test_isis_09_lfa(self): + prefix_list = 'lfa-prefix-list-test-1' + prefix_list_address = '192.168.255.255/32' + interface = 'lo' + + self.cli_set(base_path + ['net', net]) + self.cli_set(base_path + ['interface', interface]) + self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'action', 'permit']) + self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'prefix', prefix_list_address]) + + # Commit main ISIS changes + self.cli_commit() + + # Add remote portion of LFA with prefix list with validation + for level in ['level-1', 'level-2']: + self.cli_set(base_path + ['fast-reroute', 'lfa', 'remote', 'prefix-list', prefix_list, level]) + self.cli_commit() + tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' fast-reroute remote-lfa prefix-list {prefix_list} {level}', tmp) + self.cli_delete(base_path + ['fast-reroute']) + self.cli_commit() + + # Add local portion of LFA load-sharing portion with validation + for level in ['level-1', 'level-2']: + self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'load-sharing', 'disable', level]) + self.cli_commit() + tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' fast-reroute load-sharing disable {level}', tmp) + self.cli_delete(base_path + ['fast-reroute']) + self.cli_commit() + + # Add local portion of LFA priority-limit portion with validation + for priority in ['critical', 'high', 'medium']: + for level in ['level-1', 'level-2']: + self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'priority-limit', priority, level]) + self.cli_commit() + tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' fast-reroute priority-limit {priority} {level}', tmp) + self.cli_delete(base_path + ['fast-reroute']) + self.cli_commit() + + # Add local portion of LFA tiebreaker portion with validation + index = '100' + for tiebreaker in ['downstream','lowest-backup-metric','node-protecting']: + for level in ['level-1', 'level-2']: + self.cli_set(base_path + ['fast-reroute', 'lfa', 'local', 'tiebreaker', tiebreaker, 'index', index, level]) + self.cli_commit() + tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' fast-reroute lfa tiebreaker {tiebreaker} index {index} {level}', tmp) + self.cli_delete(base_path + ['fast-reroute']) + self.cli_commit() + + # Clean up and remove prefix list + self.cli_delete(['policy', 'prefix-list', prefix_list]) + self.cli_commit() + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2)
\ No newline at end of file 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/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index acabc0070..fe213a8ae 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -100,7 +100,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'daemon=300', ddclient_conf) self.assertIn(f'usev4=ifv4', ddclient_conf) self.assertIn(f'ifv4={interface}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) for opt in details.keys(): if opt == 'username': @@ -112,7 +112,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv6 only DDNS service configuration def test_02_dyndns_service_ipv6(self): - timeout = '60' + interval = '60' svc_path = ['address', interface, 'service', 'dynv6'] proto = 'dyndns2' ip_version = 'ipv6' @@ -120,7 +120,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_good = '3600' expiry_time_bad = '360' - self.cli_set(base_path + ['timeout', timeout]) + self.cli_set(base_path + ['interval', interval]) self.cli_set(base_path + svc_path + ['ip-version', ip_version]) self.cli_set(base_path + svc_path + ['protocol', proto]) self.cli_set(base_path + svc_path + ['server', server]) @@ -140,13 +140,13 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') - self.assertIn(f'daemon={timeout}', ddclient_conf) + self.assertIn(f'daemon={interval}', ddclient_conf) self.assertIn(f'usev6=ifv6', ddclient_conf) self.assertIn(f'ifv6={interface}', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'login={username}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) self.assertIn(f'min-interval={wait_time}', ddclient_conf) self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf) @@ -185,7 +185,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'usev6=ifv6', ddclient_conf) self.assertIn(f'ifv4={interface}', ddclient_conf) self.assertIn(f'ifv6={interface}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) for opt in details.keys(): if opt == 'username': @@ -218,7 +218,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'protocol=nsupdate', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'zone={zone}', ddclient_conf) - self.assertIn(f'password={key_file.name}', ddclient_conf) + self.assertIn(f'password=\'{key_file.name}\'', ddclient_conf) self.assertIn(f'ttl={ttl}', ddclient_conf) def test_05_dyndns_hostname(self): @@ -242,14 +242,67 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'login={username}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) self.assertIn(f'{name}', ddclient_conf) - def test_06_dyndns_vrf(self): - vrf_name = f'vyos-test-{"".join(random.choices(string.ascii_letters + string.digits, k=5))}' + def test_06_dyndns_web_options(self): + # Check if DDNS service can be configured and runs + base_path_iface = base_path + ['address', interface] + base_path_web = base_path + ['address', 'web'] + svc_path_iface = base_path_iface + ['service', 'cloudflare'] + svc_path_web = base_path_web + ['service', 'cloudflare'] + proto = 'cloudflare' + web_url_good = 'https://ifconfig.me/ip' + web_url_bad = 'http:/ifconfig.me/ip' + + self.cli_set(svc_path_iface + ['protocol', proto]) + self.cli_set(svc_path_iface + ['zone', zone]) + self.cli_set(svc_path_iface + ['password', password]) + self.cli_set(svc_path_iface + ['host-name', hostname]) + self.cli_set(base_path_iface + ['web-options', 'url', web_url_good]) + + # web-options is supported only with web service based address lookup + # exception is raised for interface based address lookup + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path_iface + ['web-options']) + + # commit changes + self.cli_commit() + + # web-options is supported with web service based address lookup + # this should work, but clear interface based config first + self.cli_delete(base_path_iface) + self.cli_set(svc_path_web + ['protocol', proto]) + self.cli_set(svc_path_web + ['zone', zone]) + self.cli_set(svc_path_web + ['password', password]) + self.cli_set(svc_path_web + ['host-name', hostname]) + + # web-options must be a valid URL + with self.assertRaises(ConfigSessionError) as cm: + self.cli_set(base_path_web + ['web-options', 'url', web_url_bad]) + self.assertIn(f'"{web_url_bad.removeprefix("http:")}" is not a valid URI', str(cm.exception)) + self.cli_set(base_path_web + ['web-options', 'url', web_url_good]) + + # commit changes + self.cli_commit() + + # Check the generating config parameters + ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') + self.assertIn(f'usev4=webv4', ddclient_conf) + self.assertIn(f'webv4={web_url_good}', ddclient_conf) + self.assertIn(f'protocol={proto}', ddclient_conf) + self.assertIn(f'zone={zone}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) + self.assertIn(f'{hostname}', ddclient_conf) + + def test_07_dyndns_vrf(self): + # Table number randomized, but should be within range 100-65535 + vrf_table = "".join(random.choices(string.digits, k=4)) + vrf_name = f'vyos-test-{vrf_table}' svc_path = ['address', interface, 'service', 'cloudflare'] - self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) + self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table]) self.cli_set(base_path + ['vrf', vrf_name]) self.cli_set(base_path + svc_path + ['protocol', 'cloudflare']) diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 963784f0a..969abd3d5 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -144,7 +144,7 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): subnet = '172.18.0.0/24' fwmark = '223' - limiter = 'htb' + limiter = 'tbf' self.set(['client-ip-pool', 'subnet', subnet]) diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py new file mode 100755 index 000000000..933a5704c --- /dev/null +++ b/smoketest/scripts/cli/test_system_syslog.py @@ -0,0 +1,85 @@ +#!/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 re +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.template import is_ipv4 +from vyos.template import address_from_cidr +from vyos.utils.process import call +from vyos.utils.process import DEVNULL +from vyos.utils.file import read_file +from vyos.utils.process import process_named_running +from vyos.version import get_version_data + +PROCESS_NAME = 'rsyslogd' +RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf' + +base_path = ['system', 'syslog'] + +def get_config_value(key): + tmp = read_file(RSYSLOG_CONF) + tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) + return tmp[0] + +class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestRSYSLOGService, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + # delete testing SYSLOG config + self.cli_delete(base_path) + self.cli_commit() + + # Check for running process + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_syslog_basic(self): + host1 = '198.51.100.1' + host2 = '192.0.2.1' + + self.cli_set(base_path + ['host', host1, 'port', '999']) + self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all']) + self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err']) + self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning']) + + + self.cli_commit() + # verify log level and facilities in config file + # *.warning /dev/console + # *.* @198.51.100.1:999 + # kern.err @192.0.2.1:514 + config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')] + expected = ['@198.51.100.1:999', '@192.0.2.1:514', '/dev/console'] + + for i in range(0,3): + self.assertIn(expected[i], config[i]) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index 3b4eff4d6..c8855b5d1 100755 --- a/src/completion/list_ddclient_protocols.sh +++ b/src/completion/list_ddclient_protocols.sh @@ -14,4 +14,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns') +echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 874c4b689..2bccaee0f 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,21 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_necessary = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username -username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] +username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', + 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', + 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'gandi', 'hetzner', 'dnsexit', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] # Protocols that support both IPv4 and IPv6 -dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] +dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', + 'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak', + 'njalla'] # dyndns2 protocol in ddclient honors dual stack for selective servers # because of the way it is implemented in ddclient @@ -82,34 +87,37 @@ def verify(dyndns): f'based Dynamic DNS service on "{address}"') # Dynamic DNS service provider - configuration validation + if 'web_options' in dyndns['address'][address] and address != 'web': + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + + # Dynamic DNS service provider - configuration validation if 'service' in dyndns['address'][address]: for service, config in dyndns['address'][address]['service'].items(): - error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"' + error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' for field in ['host_name', 'password', 'protocol']: if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg}') + raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in zone_necessary and 'zone' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "zone"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns}') if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg}') + raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "ttl"') + raise ConfigError(f'"ttl" {error_msg_uns}') if config['ip_version'] == 'both': if config['protocol'] not in dualstack_supported: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns}') # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time for "{config["server"]}"') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}"') if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): raise ConfigError(f'"expiry-time" must be greater than "wait-time"') diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index f6480ab0a..ceed0cf31 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -272,7 +272,7 @@ def verify_rule(firewall, rule_conf, ipv6): raise ConfigError(f'{side} port-group and port cannot both be defined') if 'log_options' in rule_conf: - if 'log' not in rule_conf or 'enable' not in rule_conf['log']: + if 'log' not in rule_conf: raise ConfigError('log-options defined, but log is not enable') if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: @@ -283,8 +283,8 @@ def verify_rule(firewall, rule_conf, ipv6): for direction in ['inbound_interface','outbound_interface']: if direction in rule_conf: - if 'interface_name' in rule_conf[direction] and 'interface_group' in rule_conf[direction]: - raise ConfigError(f'Cannot specify both interface-group and interface-name for {direction}') + if 'name' in rule_conf[direction] and 'group' in rule_conf[direction]: + raise ConfigError(f'Cannot specify both interface group and interface name for {direction}') def verify_nested_group(group_name, group, groups, seen): if 'include' not in group: @@ -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..8184d8415 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# 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 @@ -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,11 +33,16 @@ 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.template import render_to_string 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 frr from vyos import airbag airbag.enable() @@ -90,7 +94,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 +105,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 +123,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 +170,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 +230,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') @@ -223,17 +249,38 @@ def verify(bond): return None def generate(bond): + bond['frr_zebra_config'] = '' + if 'deleted' not in bond: + bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond) return None def apply(bond): - b = BondIf(bond['ifname']) - + ifname = bond['ifname'] + b = BondIf(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') + + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) + if 'frr_zebra_config' in bond: + frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index c82f01e53..31508a3c5 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# 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 @@ -28,7 +28,8 @@ from vyos.configverify import verify_vrf from vyos.ifconfig import BridgeIf from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured - +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.utils.dict import dict_search from vyos import ConfigError @@ -83,6 +84,12 @@ def get_config(config=None): if 'enable_vlan' in bridge and tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to re-create + # VLAN to VNI mappings if required + if interface.startswith('vxlan'): + set_dependents('vxlan', conf, interface) + # delete empty dictionary keys - no need to run code paths if nothing is there to do if 'member' in bridge: if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0: @@ -159,6 +166,13 @@ def apply(bridge): else: br.update(bridge) + for interface in dict_search('member.interface', bridge) or []: + if interface.startswith('vxlan'): + try: + call_dependents() + except ConfigError: + raise ConfigError('Error in updating VXLAN interface after changing bridge!') + 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-pppoe.py b/src/conf_mode/interfaces-pppoe.py index fca91253c..0a03a172c 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -77,6 +77,11 @@ def verify(pppoe): if {'connect_on_demand', 'vrf'} <= set(pppoe): raise ConfigError('On-demand dialing and VRF can not be used at the same time') + # both MTU and MRU have default values, thus we do not need to check + # if the key exists + if int(pppoe['mru']) > int(pppoe['mtu']): + raise ConfigError('PPPoE MRU needs to be lower then MTU!') + return None def generate(pppoe): diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 05f68112a..6bf3227d5 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -34,6 +34,7 @@ from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import Interface from vyos.ifconfig import VXLANIf from vyos.template import is_ipv6 +from vyos.utils.dict import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() @@ -53,7 +54,7 @@ def get_config(config=None): # VXLAN interfaces are picky and require recreation if certain parameters # change. But a VXLAN interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['parameters', 'external', 'gpe', 'group', 'port', 'remote', + for cli_option in ['parameters', 'gpe', 'group', 'port', 'remote', 'source-address', 'source-interface', 'vni']: if is_node_changed(conf, base + [ifname, cli_option]): vxlan.update({'rebuild_required': {}}) @@ -94,17 +95,17 @@ def verify(vxlan): if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan): raise ConfigError('Group, remote, source-address or source-interface must be configured') - if 'vni' not in vxlan and 'external' not in vxlan: - raise ConfigError( - 'Must either configure VXLAN "vni" or use "external" CLI option!') + if 'vni' not in vxlan and dict_search('parameters.external', vxlan) == None: + raise ConfigError('Must either configure VXLAN "vni" or use "external" CLI option!') - if {'external', 'vni'} <= set(vxlan): - raise ConfigError('Can not specify both "external" and "VNI"!') + if dict_search('parameters.external', vxlan): + if 'vni' in vxlan: + raise ConfigError('Can not specify both "external" and "VNI"!') - if {'external', 'other_tunnels'} <= set(vxlan): - other_tunnels = ', '.join(vxlan['other_tunnels']) - raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\ - f'CLI option is used. Additional tunnels: {other_tunnels}') + if 'other_tunnels' in vxlan: + other_tunnels = ', '.join(vxlan['other_tunnels']) + raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\ + f'CLI option is used. Additional tunnels: {other_tunnels}') if 'gpe' in vxlan and 'external' not in vxlan: raise ConfigError(f'VXLAN-GPE is only supported when "external" '\ @@ -164,10 +165,22 @@ def verify(vxlan): raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!') vnis_used.append(vni) + if dict_search('parameters.neighbor_suppress', vxlan): + if 'is_bridge_member' not in vxlan: + raise ConfigError('Neighbor suppression requires that VXLAN interface '\ + 'is member of a bridge interface!') + verify_mtu_ipv6(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/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 122d9589a..79e5d3f44 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -51,17 +51,9 @@ def get_config(config=None): tmp = is_node_changed(conf, base + [ifname, 'port']) if tmp: wireguard['port_changed'] = {} - # Determine which Wireguard peer has been removed. - # Peers can only be removed with their public key! - if 'peer' in wireguard: - peer_remove = {} - for peer, peer_config in wireguard['peer'].items(): - # T4702: If anything on a peer changes we remove the peer first and re-add it - if is_node_changed(conf, base + [ifname, 'peer', peer]): - if 'public_key' in peer_config: - peer_remove = dict_merge({'peer_remove' : {peer : peer_config['public_key']}}, peer_remove) - if peer_remove: - wireguard.update(peer_remove) + # T4702: If anything on a peer changes we remove the peer first and re-add it + if is_node_changed(conf, base + [ifname, 'peer']): + wireguard.update({'rebuild_required': {}}) return wireguard @@ -113,12 +105,21 @@ def verify(wireguard): public_keys.append(peer['public_key']) def apply(wireguard): - tmp = WireGuardIf(wireguard['ifname']) - if 'deleted' in wireguard: - tmp.remove() - return None + if 'rebuild_required' in wireguard or 'deleted' in wireguard: + wg = WireGuardIf(**wireguard) + # WireGuard only supports peer removal based on the configured public-key, + # by deleting the entire interface this is the shortcut instead of parsing + # out all peers and removing them one by one. + # + # Peer reconfiguration will always come with a short downtime while the + # WireGuard interface is recreated (see below) + wg.remove() + + # Create the new interface if required + if 'deleted' not in wireguard: + wg = WireGuardIf(**wireguard) + wg.update(wireguard) - tmp.update(wireguard) return None if __name__ == '__main__': diff --git a/src/conf_mode/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py index 8fe429653..ec4311bb5 100755 --- a/src/conf_mode/load-balancing-haproxy.py +++ b/src/conf_mode/load-balancing-haproxy.py @@ -94,8 +94,8 @@ def generate(lb): if os.path.isfile(file): os.unlink(file) # Delete old directories - #if os.path.isdir(load_balancing_dir): - # rmtree(load_balancing_dir, ignore_errors=True) + if os.path.isdir(load_balancing_dir): + rmtree(load_balancing_dir, ignore_errors=True) return None @@ -106,15 +106,12 @@ def generate(lb): # SSL Certificates for frontend for front, front_config in lb['service'].items(): if 'ssl' in front_config: - cert_file_path = os.path.join(load_balancing_dir, 'cert.pem') - cert_key_path = os.path.join(load_balancing_dir, 'cert.pem.key') - ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem') if 'certificate' in front_config['ssl']: - #cert_file_path = os.path.join(load_balancing_dir, 'cert.pem') - #cert_key_path = os.path.join(load_balancing_dir, 'cert.key') cert_name = front_config['ssl']['certificate'] pki_cert = lb['pki']['certificate'][cert_name] + cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem') + cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key') with open(cert_file_path, 'w') as f: f.write(wrap_certificate(pki_cert['certificate'])) @@ -126,6 +123,7 @@ def generate(lb): if 'ca_certificate' in front_config['ssl']: ca_name = front_config['ssl']['ca_certificate'] pki_ca_cert = lb['pki']['ca'][ca_name] + ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') with open(ca_cert_file_path, 'w') as f: f.write(wrap_certificate(pki_ca_cert['certificate'])) @@ -133,11 +131,11 @@ def generate(lb): # SSL Certificates for backend for back, back_config in lb['backend'].items(): if 'ssl' in back_config: - ca_cert_file_path = os.path.join(load_balancing_dir, 'ca.pem') if 'ca_certificate' in back_config['ssl']: ca_name = back_config['ssl']['ca_certificate'] pki_ca_cert = lb['pki']['ca'][ca_name] + ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem') with open(ca_cert_file_path, 'w') as f: f.write(wrap_certificate(pki_ca_cert['certificate'])) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 52a7a71fd..44b13d413 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -151,8 +151,11 @@ def verify(nat): err_msg = f'Source NAT configuration error in rule {rule}:' if 'outbound_interface' in config: - if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']: + raise ConfigError(f'{err_msg} - Cannot specify both interface group and interface name for nat source rule "{rule}"') + elif 'name' in config['outbound_interface']: + if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces(): + Warning(f'{err_msg} - interface "{config["outbound_interface"]["name"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config): if 'exclude' not in config and 'backend' not in config['load_balance']: @@ -172,8 +175,11 @@ def verify(nat): err_msg = f'Destination NAT configuration error in rule {rule}:' if 'inbound_interface' in config: - if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') + if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']: + raise ConfigError(f'{err_msg} - Cannot specify both interface group and interface name for destination nat rule "{rule}"') + elif 'name' in config['inbound_interface']: + if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces(): + Warning(f'{err_msg} - interface "{config["inbound_interface"]["name"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']: if 'exclude' not in config and 'backend' not in config['load_balance']: diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 46d796bc8..0ba08aef3 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -62,11 +62,13 @@ def verify(nat): if dict_search('source.rule', nat): for rule, config in dict_search('source.rule', nat).items(): err_msg = f'Source NAT66 configuration error in rule {rule}:' - if 'outbound_interface' not in config: - raise ConfigError(f'{err_msg} outbound-interface not specified') - if config['outbound_interface'] not in interfaces(): - raise ConfigError(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + if 'outbound_interface' in config: + if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']: + raise ConfigError(f'{err_msg} - Cannot specify both interface group and interface name for nat source rule "{rule}"') + elif 'name' in config['outbound_interface']: + if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces(): + Warning(f'{err_msg} - interface "{config["outbound_interface"]["name"]}" does not exist on this system') addr = dict_search('translation.address', config) if addr != None: @@ -85,12 +87,12 @@ def verify(nat): for rule, config in dict_search('destination.rule', nat).items(): err_msg = f'Destination NAT66 configuration error in rule {rule}:' - if 'inbound_interface' not in config: - raise ConfigError(f'{err_msg}\n' \ - 'inbound-interface not specified') - else: - if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') + if 'inbound_interface' in config: + if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']: + raise ConfigError(f'{err_msg} - Cannot specify both interface group and interface name for destination nat rule "{rule}"') + elif 'name' in config['inbound_interface']: + if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces(): + Warning(f'{err_msg} - interface "{config["inbound_interface"]["name"]}" does not exist on this system') return None 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/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index e00c58ee4..ce67ccff7 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -48,7 +48,8 @@ def get_config(config=None): # eqivalent of the C foo ? 'a' : 'b' statement base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path isis = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) + get_first_key=True, + no_tag_node_value_mangle=True) # Assign the name of our VRF context. This MUST be done before the return # statement below, else on deletion we will delete the default instance @@ -219,6 +220,38 @@ def verify(isis): if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') + + # Check for LFA tiebreaker index duplication + if dict_search('fast_reroute.lfa.local.tiebreaker', isis): + comparison_dictionary = {} + for item, item_options in isis['fast_reroute']['lfa']['local']['tiebreaker'].items(): + for index, index_options in item_options.items(): + for index_value, index_value_options in index_options.items(): + if index_value not in comparison_dictionary.keys(): + comparison_dictionary[index_value] = [item] + else: + comparison_dictionary[index_value].append(item) + for index, index_length in comparison_dictionary.items(): + if int(len(index_length)) > 1: + raise ConfigError(f'LFA index {index} cannot have more than one tiebreaker configured.') + + # Check for LFA priority-limit configured multiple times per level + if dict_search('fast_reroute.lfa.local.priority_limit', isis): + comparison_dictionary = {} + for priority, priority_options in isis['fast_reroute']['lfa']['local']['priority_limit'].items(): + for level, level_options in priority_options.items(): + if level not in comparison_dictionary.keys(): + comparison_dictionary[level] = [priority] + else: + comparison_dictionary[level].append(priority) + for level, level_length in comparison_dictionary.items(): + if int(len(level_length)) > 1: + raise ConfigError(f'LFA priority-limit on {level.replace("_", "-")} cannot have more than one priority configured.') + + # Check for LFA remote prefix list configured with more than one list + if dict_search('fast_reroute.lfa.remote.prefix_list', isis): + if int(len(isis['fast_reroute']['lfa']['remote']['prefix_list'].items())) > 1: + raise ConfigError(f'LFA remote prefix-list has more than one configured. Cannot have more than one configured.') return None @@ -265,4 +298,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - exit(1) + exit(1)
\ No newline at end of file diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index cddd3765e..2f07142a3 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit from sys import argv diff --git a/src/conf_mode/protocols_static_neighbor-proxy.py b/src/conf_mode/protocols_static_neighbor-proxy.py new file mode 100755 index 000000000..10cc1e748 --- /dev/null +++ b/src/conf_mode/protocols_static_neighbor-proxy.py @@ -0,0 +1,95 @@ +#!/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 + +from sys import exit + +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['protocols', 'static', 'neighbor-proxy'] + config = conf.get_config_dict(base, get_first_key=True) + + return config + + +def verify(config): + + if 'arp' in config: + for neighbor, neighbor_conf in config['arp'].items(): + if 'interface' not in neighbor_conf: + raise ConfigError( + f"ARP neighbor-proxy for '{neighbor}' requires an interface to be set!" + ) + + if 'nd' in config: + for neighbor, neighbor_conf in config['nd'].items(): + if 'interface' not in neighbor_conf: + raise ConfigError( + f"ARP neighbor-proxy for '{neighbor}' requires an interface to be set!" + ) + + +def generate(config): + pass + + +def apply(config): + if not config: + # Cleanup proxy + call('ip neighbor flush proxy') + call('ip -6 neighbor flush proxy') + return None + + # Add proxy ARP + if 'arp' in config: + # Cleanup entries before config + call('ip neighbor flush proxy') + for neighbor, neighbor_conf in config['arp'].items(): + for interface in neighbor_conf.get('interface'): + call(f'ip neighbor add proxy {neighbor} dev {interface}') + + # Add proxy NDP + if 'nd' in config: + # Cleanup entries before config + call('ip -6 neighbor flush proxy') + for neighbor, neighbor_conf in config['nd'].items(): + for interface in neighbor_conf['interface']: + call(f'ip -6 neighbor add proxy {neighbor} dev {interface}') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py index 6909731ff..6526c23d1 100755 --- a/src/conf_mode/service_mdns-repeater.py +++ b/src/conf_mode/service_mdns-repeater.py @@ -29,6 +29,7 @@ from vyos import airbag airbag.enable() config_file = '/run/avahi-daemon/avahi-daemon.conf' +systemd_override = r'/run/systemd/system/avahi-daemon.service.d/override.conf' vrrp_running_file = '/run/mdns_vrrp_active' def get_config(config=None): @@ -48,6 +49,8 @@ def get_config(config=None): if mdns: mdns['vrrp_exists'] = conf.exists('high-availability vrrp') + mdns['config_file'] = config_file + return mdns def verify(mdns): @@ -101,11 +104,16 @@ def generate(mdns): return None render(config_file, 'mdns-repeater/avahi-daemon.conf.j2', mdns) + render(systemd_override, 'mdns-repeater/override.conf.j2', mdns) return None def apply(mdns): + systemd_service = 'avahi-daemon.service' + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not mdns or 'disable' in mdns: - call('systemctl stop avahi-daemon.service') + call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) @@ -120,10 +128,10 @@ def apply(mdns): os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater if len(mdns['interface']) < 2: - call('systemctl stop avahi-daemon.service') + call(f'systemctl stop {systemd_service}') return None - call('systemctl restart avahi-daemon.service') + call(f'systemctl restart {systemd_service}') return None diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index aace267a7..87660c127 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -48,9 +48,12 @@ def get_config(config=None): # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 - if is_node_changed(conf, base + ['client-ip-pool']) or is_node_changed( - conf, base + ['client-ipv6-pool']): + conditions = [is_node_changed(conf, base + ['client-ip-pool']), + is_node_changed(conf, base + ['client-ipv6-pool']), + is_node_changed(conf, base + ['interface'])] + if any(conditions): pppoe.update({'restart_required': {}}) + return pppoe def verify(pppoe): diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py deleted file mode 100755 index 82c2f236e..000000000 --- a/src/conf_mode/vpp.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/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 -from psutil import virtual_memory - -from pathlib import Path -from re import search as re_search, MULTILINE as re_M - -from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents -from vyos.configdict import node_changed -from vyos.ifconfig import Section -from vyos.utils.boot import boot_configuration_complete -from vyos.utils.process import call -from vyos.utils.process import rc_cmd -from vyos.utils.system import sysctl_read -from vyos.utils.system import sysctl_apply -from vyos.template import render - -from vyos import ConfigError -from vyos import airbag -from vyos.vpp import VPPControl -from vyos.vpp import HostControl - -airbag.enable() - -service_name = 'vpp' -service_conf = Path(f'/run/vpp/{service_name}.conf') -systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' - -# Free memory required for VPP -# 2 GB for hugepages + 1 GB for other services -MIN_AVAILABLE_MEMORY: int = 3 * 1024**3 - - -def _get_pci_address_by_interface(iface) -> str: - rc, out = rc_cmd(f'ethtool -i {iface}') - # if ethtool command was successful - if rc == 0 and out: - regex_filter = r'^bus-info: (?P<address>\w+:\w+:\w+\.\w+)$' - re_obj = re_search(regex_filter, out, re_M) - # if bus-info with PCI address found - if re_obj: - address = re_obj.groupdict().get('address', '') - return address - # use VPP - maybe interface already attached to it - vpp_control = VPPControl(attempts=20, interval=500) - pci_addr = vpp_control.get_pci_addr(iface) - if pci_addr: - return pci_addr - # raise error if PCI address was not found - raise ConfigError(f'Cannot find PCI address for interface {iface}') - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['vpp'] - base_ethernet = ['interfaces', 'ethernet'] - - # find interfaces removed from VPP - removed_ifaces = [] - tmp = node_changed(conf, base + ['interface']) - if tmp: - for removed_iface in tmp: - pci_address: str = _get_pci_address_by_interface(removed_iface) - removed_ifaces.append({ - 'iface_name': removed_iface, - 'iface_pci_addr': pci_address - }) - # add an interface to a list of interfaces that need - # to be reinitialized after the commit - set_dependents('ethernet', conf, removed_iface) - - if not conf.exists(base): - return {'removed_ifaces': removed_ifaces} - - config = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - if 'interface' in config: - for iface, iface_config in config['interface'].items(): - # add an interface to a list of interfaces that need - # to be reinitialized after the commit - set_dependents('ethernet', conf, iface) - - # Get PCI address auto - if iface_config['pci'] == 'auto': - config['interface'][iface]['pci'] = _get_pci_address_by_interface(iface) - - config['other_interfaces'] = conf.get_config_dict(base_ethernet, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - if removed_ifaces: - config['removed_ifaces'] = removed_ifaces - - return config - - -def verify(config): - # bail out early - looks like removal from running config - if not config or (len(config) == 1 and 'removed_ifaces' in config): - return None - - if 'interface' not in config: - raise ConfigError('"interface" is required but not set!') - - if 'cpu' in config: - if 'corelist_workers' in config['cpu'] and 'main_core' not in config[ - 'cpu']: - raise ConfigError('"cpu main-core" is required but not set!') - - memory_available: int = virtual_memory().available - if memory_available < MIN_AVAILABLE_MEMORY: - raise ConfigError( - 'Not enough free memory to start VPP:\n' - f'available: {round(memory_available / 1024**3, 1)}GB\n' - f'required: {round(MIN_AVAILABLE_MEMORY / 1024**3, 1)}GB') - - -def generate(config): - if not config or (len(config) == 1 and 'removed_ifaces' in config): - # Remove old config and return - service_conf.unlink(missing_ok=True) - return None - - render(service_conf, 'vpp/startup.conf.j2', config) - render(systemd_override, 'vpp/override.conf.j2', config) - - # apply default sysctl values from - # https://github.com/FDio/vpp/blob/v23.06/src/vpp/conf/80-vpp.conf - sysctl_config: dict[str, str] = { - 'vm.nr_hugepages': '1024', - 'vm.max_map_count': '3096', - 'vm.hugetlb_shm_group': '0', - 'kernel.shmmax': '2147483648' - } - # we do not want to reduce `kernel.shmmax` - kernel_shmnax_current: str = sysctl_read('kernel.shmmax') - if int(kernel_shmnax_current) > int(sysctl_config['kernel.shmmax']): - sysctl_config['kernel.shmmax'] = kernel_shmnax_current - - if not sysctl_apply(sysctl_config): - raise ConfigError('Cannot configure sysctl parameters for VPP') - - return None - - -def apply(config): - if not config or (len(config) == 1 and 'removed_ifaces' in config): - call(f'systemctl stop {service_name}.service') - else: - call('systemctl daemon-reload') - call(f'systemctl restart {service_name}.service') - - # Initialize interfaces removed from VPP - for iface in config.get('removed_ifaces', []): - host_control = HostControl() - # rescan PCI to use a proper driver - host_control.pci_rescan(iface['iface_pci_addr']) - # rename to the proper name - iface_new_name: str = host_control.get_eth_name(iface['iface_pci_addr']) - host_control.rename_iface(iface_new_name, iface['iface_name']) - - if 'interface' in config: - # connect to VPP - # must be performed multiple attempts because API is not available - # immediately after the service restart - vpp_control = VPPControl(attempts=20, interval=500) - for iface, _ in config['interface'].items(): - # Create lcp - if iface not in Section.interfaces(): - vpp_control.lcp_pair_add(iface, iface) - - # reinitialize interfaces, but not during the first boot - if boot_configuration_complete(): - call_dependents() - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks index b4b4d516d..570758be6 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks +++ b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks @@ -1,5 +1,5 @@ #!/bin/bash DHCP_PRE_HOOKS="/config/scripts/dhcp-client/pre-hooks.d/" if [ -d "${DHCP_PRE_HOOKS}" ] ; then - run-parts "${DHCP_PRE_HOOKS}" + run_hookdir "${DHCP_PRE_HOOKS}" fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks index 442419d79..910b586f0 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks +++ b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks @@ -1,5 +1,5 @@ #!/bin/bash DHCP_POST_HOOKS="/config/scripts/dhcp-client/post-hooks.d/" if [ -d "${DHCP_POST_HOOKS}" ] ; then - run-parts "${DHCP_POST_HOOKS}" + run_hookdir "${DHCP_POST_HOOKS}" fi diff --git a/src/etc/sudoers.d/vyos b/src/etc/sudoers.d/vyos index e0fd8cb0b..c099446ba 100644 --- a/src/etc/sudoers.d/vyos +++ b/src/etc/sudoers.d/vyos @@ -42,6 +42,7 @@ Cmnd_Alias FORCE_CLUSTER = /usr/share/heartbeat/hb_takeover, \ /usr/share/heartbeat/hb_standby Cmnd_Alias DIAGNOSTICS = /bin/ip vrf exec * /bin/ping *, \ /bin/ip vrf exec * /bin/traceroute *, \ + /bin/ip vrf exec * /usr/bin/mtr *, \ /usr/libexec/vyos/op_mode/* %operator ALL=NOPASSWD: DATE, IPTABLES, ETHTOOL, IPFLUSH, HWINFO, \ PPPOE_CMDS, PCAPTURE, /usr/sbin/wanpipemon, \ diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index fcdc1b21d..1c9b8999f 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -21,7 +21,6 @@ net.ipv4.conf.all.arp_filter=0 # https://vyos.dev/T300 net.ipv4.conf.all.arp_ignore=0 - net.ipv4.conf.all.arp_announce=2 # Enable packet forwarding for IPv4 @@ -103,6 +102,6 @@ net.ipv4.igmp_max_memberships = 512 net.core.rps_sock_flow_entries = 32768 # Congestion control -net.core.default_qdisc=fq +net.core.default_qdisc=fq_codel net.ipv4.tcp_congestion_control=bbr diff --git a/src/etc/systemd/system/avahi-daemon.service.d/override.conf b/src/etc/systemd/system/avahi-daemon.service.d/override.conf deleted file mode 100644 index a9d2085f7..000000000 --- a/src/etc/systemd/system/avahi-daemon.service.d/override.conf +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -After= -After=vyos-router.service -ConditionPathExists=/run/avahi-daemon/avahi-daemon.conf - -[Service] -ExecStart= -ExecStart=/usr/sbin/avahi-daemon --syslog --file /run/avahi-daemon/avahi-daemon.conf
\ No newline at end of file diff --git a/src/etc/udev/rules.d/99-vyos-systemd.rules b/src/etc/udev/rules.d/99-vyos-systemd.rules new file mode 100644 index 000000000..54aea668c --- /dev/null +++ b/src/etc/udev/rules.d/99-vyos-systemd.rules @@ -0,0 +1,79 @@ +# The main reason that we store this file is systemd-udevd interfaces excludes +# /lib/systemd/systemd-sysctl for dynamic interfaces (ppp|ipoe|l2tp etc) + +ACTION=="remove", GOTO="systemd_end" + +SUBSYSTEM=="tty", KERNEL=="tty[a-zA-Z]*|hvc*|xvc*|hvsi*|ttysclp*|sclp_line*|3270/tty[0-9]*", TAG+="systemd" +KERNEL=="vport*", TAG+="systemd" + +SUBSYSTEM=="ptp", TAG+="systemd" + +SUBSYSTEM=="ubi", TAG+="systemd" + +SUBSYSTEM=="block", TAG+="systemd" + +# We can't make any conclusions about suspended DM devices so let's just import previous SYSTEMD_READY state and skip other rules +SUBSYSTEM=="block", ENV{DM_SUSPENDED}=="1", IMPORT{db}="SYSTEMD_READY", GOTO="systemd_end" +SUBSYSTEM=="block", ACTION=="add", ENV{DM_UDEV_DISABLE_OTHER_RULES_FLAG}=="1", ENV{SYSTEMD_READY}="0" + +# Ignore encrypted devices with no identified superblock on it, since +# we are probably still calling mke2fs or mkswap on it. +SUBSYSTEM=="block", ENV{DM_UUID}=="CRYPT-*", ENV{ID_PART_TABLE_TYPE}=="", ENV{ID_FS_USAGE}=="", ENV{SYSTEMD_READY}="0" + +# Explicitly set SYSTEMD_READY=1 for DM devices that don't have it set yet, so that we always have something to import above +SUBSYSTEM=="block", ENV{DM_UUID}=="?*", ENV{SYSTEMD_READY}=="", ENV{SYSTEMD_READY}="1" + +# add symlink to GPT root disk +SUBSYSTEM=="block", ENV{ID_PART_GPT_AUTO_ROOT}=="1", ENV{ID_FS_TYPE}!="crypto_LUKS", SYMLINK+="gpt-auto-root" +SUBSYSTEM=="block", ENV{ID_PART_GPT_AUTO_ROOT}=="1", ENV{ID_FS_TYPE}=="crypto_LUKS", SYMLINK+="gpt-auto-root-luks" +SUBSYSTEM=="block", ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="root", SYMLINK+="gpt-auto-root" + +# Ignore raid devices that are not yet assembled and started +SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="md*", TEST!="md/array_state", ENV{SYSTEMD_READY}="0" +SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="md*", ATTR{md/array_state}=="|clear|inactive", ENV{SYSTEMD_READY}="0" + +# Ignore loop devices that don't have any file attached +SUBSYSTEM=="block", KERNEL=="loop[0-9]*", ENV{DEVTYPE}=="disk", TEST!="loop/backing_file", ENV{SYSTEMD_READY}="0" + +# Ignore nbd devices until the PID file exists (which signals a connected device) +SUBSYSTEM=="block", KERNEL=="nbd*", ENV{DEVTYPE}=="disk", TEST!="pid", ENV{SYSTEMD_READY}="0" + +# We need a hardware independent way to identify network devices. We +# use the /sys/subsystem/ path for this. Kernel "bus" and "class" names +# should be treated as one namespace, like udev handles it. This is mostly +# just an identification string for systemd, so whether the path actually is +# accessible or not does not matter as long as it is unique and in the +# filesystem namespace. + +SUBSYSTEM=="net", KERNEL!="lo", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/net/devices/$name" +SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/bluetooth/devices/%k", \ + ENV{SYSTEMD_WANTS}+="bluetooth.target", ENV{SYSTEMD_USER_WANTS}+="bluetooth.target" + +ENV{ID_SMARTCARD_READER}=="?*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="smartcard.target", ENV{SYSTEMD_USER_WANTS}+="smartcard.target" +SUBSYSTEM=="sound", KERNEL=="controlC*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sound.target", ENV{SYSTEMD_USER_WANTS}+="sound.target" + +SUBSYSTEM=="printer", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target" +SUBSYSTEM=="usb", KERNEL=="lp*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target" +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0701??:*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target" + +SUBSYSTEM=="udc", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}+="usb-gadget.target" + +# Apply sysctl variables to network devices (and only to those) as they appear. +# T5706. Exclude: lo, dummy*, ppp*, ipoe*, l2tp*, pptp*, sslvpn* and sstp*. +ACTION=="add", SUBSYSTEM=="net", KERNEL!="lo|dummy*|ppp*|ipoe*|l2tp*|pptp*|sslvpn*|sstp*", RUN+="/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/$name --prefix=/net/ipv4/neigh/$name --prefix=/net/ipv6/conf/$name --prefix=/net/ipv6/neigh/$name" + +# Pull in backlight save/restore for all backlight devices and +# keyboard backlights +SUBSYSTEM=="backlight", TAG+="systemd", IMPORT{builtin}="path_id", ENV{SYSTEMD_WANTS}+="systemd-backlight@backlight:$name.service" +SUBSYSTEM=="leds", KERNEL=="*kbd_backlight", TAG+="systemd", IMPORT{builtin}="path_id", ENV{SYSTEMD_WANTS}+="systemd-backlight@leds:$name.service" + +# Pull in rfkill save/restore for all rfkill devices +SUBSYSTEM=="rfkill", ENV{SYSTEMD_RFKILL}="1" +SUBSYSTEM=="rfkill", IMPORT{builtin}="path_id" +SUBSYSTEM=="misc", KERNEL=="rfkill", TAG+="systemd", ENV{SYSTEMD_WANTS}+="systemd-rfkill.socket" + +# Asynchronously mount file systems implemented by these modules as soon as they are loaded. +SUBSYSTEM=="module", KERNEL=="fuse", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sys-fs-fuse-connections.mount" +SUBSYSTEM=="module", KERNEL=="configfs", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sys-kernel-config.mount" + +LABEL="systemd_end" diff --git a/src/helpers/config_dependency.py b/src/helpers/config_dependency.py index 50c72956e..817bcc65a 100755 --- a/src/helpers/config_dependency.py +++ b/src/helpers/config_dependency.py @@ -18,22 +18,75 @@ import os import sys +import json from argparse import ArgumentParser from argparse import ArgumentTypeError - -try: - from vyos.configdep import check_dependency_graph - from vyos.defaults import directories -except ImportError: - # allow running during addon package build - _here = os.path.dirname(__file__) - sys.path.append(os.path.join(_here, '../../python/vyos')) - from configdep import check_dependency_graph - from defaults import directories +from graphlib import TopologicalSorter, CycleError # addon packages will need to specify the dependency directory -dependency_dir = os.path.join(directories['data'], - 'config-mode-dependencies') +data_dir = '/usr/share/vyos/' +dependency_dir = os.path.join(data_dir, 'config-mode-dependencies') + +def dict_merge(source, destination): + from copy import deepcopy + tmp = deepcopy(destination) + + for key, value in source.items(): + if key not in tmp: + tmp[key] = value + elif isinstance(source[key], dict): + tmp[key] = dict_merge(source[key], tmp[key]) + + return tmp + +def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict: + res = {} + for dep_file in os.listdir(dependency_dir): + if not dep_file.endswith('.json'): + continue + path = os.path.join(dependency_dir, dep_file) + with open(path) as f: + d = json.load(f) + if dep_file == 'vyos-1x.json': + res = dict_merge(res, d) + else: + res = dict_merge(d, res) + + return res + +def graph_from_dependency_dict(d: dict) -> dict: + g = {} + for k in list(d): + g[k] = set() + # add the dependencies for every sub-case; should there be cases + # that are mutally exclusive in the future, the graphs will be + # distinguished + for el in list(d[k]): + g[k] |= set(d[k][el]) + + return g + +def is_acyclic(d: dict) -> bool: + g = graph_from_dependency_dict(d) + ts = TopologicalSorter(g) + try: + # get node iterator + order = ts.static_order() + # try iteration + _ = [*order] + except CycleError: + return False + + return True + +def check_dependency_graph(dependency_dir: str = dependency_dir, + supplement: str = None) -> bool: + d = read_dependency_dict(dependency_dir=dependency_dir) + if supplement is not None: + with open(supplement) as f: + d = dict_merge(json.load(f), d) + + return is_acyclic(d) def path_exists(s): if not os.path.exists(s): @@ -50,8 +103,10 @@ def main(): args = vars(parser.parse_args()) if not check_dependency_graph(**args): + print("dependency error: cycle exists") sys.exit(1) + print("dependency graph acyclic") sys.exit(0) if __name__ == '__main__': diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py index eb584edaf..cb29069cf 100755 --- a/src/helpers/strip-private.py +++ b/src/helpers/strip-private.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-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 @@ -21,7 +21,6 @@ import sys from netaddr import IPNetwork, AddrFormatError - parser = argparse.ArgumentParser(description='strip off private information from VyOS config') strictness = parser.add_mutually_exclusive_group() @@ -119,6 +118,7 @@ if __name__ == "__main__": (True, re.compile(r'(shared-secret-key-file|ca-cert-file|cert-file|dh-file|key-file|client) (\S+)'), r'\1 xxxxxx'), # Strip IPSEC secrets (True, re.compile(r'pre-shared-secret \S+'), 'pre-shared-secret xxxxxx'), + (True, re.compile(r'secret \S+'), 'secret xxxxxx'), # Strip OSPF md5-key (True, re.compile(r'md5-key \S+'), 'md5-key xxxxxx'), # Strip WireGuard private-key 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/dns-dynamic/1-to-2 b/src/migration-scripts/dns-dynamic/1-to-2 new file mode 100755 index 000000000..8b599b57a --- /dev/null +++ b/src/migration-scripts/dns-dynamic/1-to-2 @@ -0,0 +1,70 @@ +#!/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/>. + +# T5708: +# - migrate "service dns dynamic timeout ..." +# to "service dns dynamic interval ..." +# - remove "service dns dynamic address <interface> web-options ..." when <interface> != "web" +# - migrate "service dns dynamic address <interface> service <service> protocol dnsexit" +# to "service dns dynamic address <interface> service <service> protocol dnsexit2" + +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) + +base_path = ['service', 'dns', 'dynamic'] +timeout_path = base_path + ['timeout'] +address_path = base_path + ['address'] + +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +# Migrate "service dns dynamic timeout ..." +# to "service dns dynamic interval ..." +if config.exists(timeout_path): + config.rename(timeout_path, 'interval') + +# Remove "service dns dynamic address <interface> web-options ..." when <interface> != "web" +for address in config.list_nodes(address_path): + if config.exists(address_path + [address, 'web-options']) and address != 'web': + config.delete(address_path + [address, 'web-options']) + +# Migrate "service dns dynamic address <interface> service <service> protocol dnsexit" +# to "service dns dynamic address <interface> service <service> protocol dnsexit2" +for address in config.list_nodes(address_path): + for svc_cfg in config.list_nodes(address_path + [address, 'service']): + if config.exists(address_path + [address, 'service', svc_cfg, 'protocol']): + protocol = config.return_value(address_path + [address, 'service', svc_cfg, 'protocol']) + if protocol == 'dnsexit': + config.set(address_path + [address, 'service', svc_cfg, 'protocol'], 'dnsexit2') + +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/firewall/11-to-12 b/src/migration-scripts/firewall/11-to-12 new file mode 100755 index 000000000..ba8374d66 --- /dev/null +++ b/src/migration-scripts/firewall/11-to-12 @@ -0,0 +1,74 @@ +#!/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/>. + +# T5681: Firewall re-writing. Simplify cli when mathcing interface +# From + # set firewall ... rule <rule> [inbound-interface | outboubd-interface] interface-name <iface> + # set firewall ... rule <rule> [inbound-interface | outboubd-interface] interface-group <iface_group> +# To + # set firewall ... rule <rule> [inbound-interface | outboubd-interface] name <iface> + # set firewall ... rule <rule> [inbound-interface | outboubd-interface] group <iface_group> + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +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 = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +## Migration from base chains +#if config.exists(base + ['interface', iface, direction]): +for family in ['ipv4', 'ipv6']: + if config.exists(base + [family]): + for hook in ['forward', 'input', 'output', 'name']: + if config.exists(base + [family, hook]): + for priority in config.list_nodes(base + [family, hook]): + if config.exists(base + [family, hook, priority, 'rule']): + for rule in config.list_nodes(base + [family, hook, priority, 'rule']): + for direction in ['inbound-interface', 'outbound-interface']: + if config.exists(base + [family, hook, priority, 'rule', rule, direction]): + if config.exists(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']): + iface = config.return_value(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']) + config.set(base + [family, hook, priority, 'rule', rule, direction, 'name'], value=iface) + config.delete(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']) + elif config.exists(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']): + group = config.return_value(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']) + config.set(base + [family, hook, priority, 'rule', rule, direction, 'group'], value=group) + config.delete(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']) + +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)) + exit(1)
\ No newline at end of file diff --git a/src/migration-scripts/firewall/12-to-13 b/src/migration-scripts/firewall/12-to-13 new file mode 100755 index 000000000..4eaae779b --- /dev/null +++ b/src/migration-scripts/firewall/12-to-13 @@ -0,0 +1,83 @@ +#!/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/>. + +# T5729: Switch to valueless whenever is possible. +# From + # set firewall ... rule <rule> log enable + # set firewall ... rule <rule> state <state> enable + # set firewall ... rule <rule> log disable + # set firewall ... rule <rule> state <state> disable +# To + # set firewall ... rule <rule> log + # set firewall ... rule <rule> state <state> + # Remove command if log=disable or <state>=disable + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +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 = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +for family in ['ipv4', 'ipv6', 'bridge']: + if config.exists(base + [family]): + for hook in ['forward', 'input', 'output', 'name']: + if config.exists(base + [family, hook]): + for priority in config.list_nodes(base + [family, hook]): + if config.exists(base + [family, hook, priority, 'rule']): + for rule in config.list_nodes(base + [family, hook, priority, 'rule']): + # Log + if config.exists(base + [family, hook, priority, 'rule', rule, 'log']): + log_value = config.return_value(base + [family, hook, priority, 'rule', rule, 'log']) + config.delete(base + [family, hook, priority, 'rule', rule, 'log']) + if log_value == 'enable': + config.set(base + [family, hook, priority, 'rule', rule, 'log']) + # State + if config.exists(base + [family, hook, priority, 'rule', rule, 'state']): + flag_enable = 'False' + for state in ['established', 'invalid', 'new', 'related']: + if config.exists(base + [family, hook, priority, 'rule', rule, 'state', state]): + state_value = config.return_value(base + [family, hook, priority, 'rule', rule, 'state', state]) + config.delete(base + [family, hook, priority, 'rule', rule, 'state', state]) + if state_value == 'enable': + config.set(base + [family, hook, priority, 'rule', rule, 'state'], value=state, replace=False) + flag_enable = 'True' + if flag_enable == 'False': + config.delete(base + [family, hook, priority, 'rule', rule, 'state']) + +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)) + exit(1)
\ No newline at end of file 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..ca3d19320 --- /dev/null +++ b/src/migration-scripts/interfaces/31-to-32 @@ -0,0 +1,51 @@ +#!/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 config.exists(base + [vxlan, 'external']): + config.delete(base + [vxlan, 'external']) + config.set(base + [vxlan, 'parameters', 'external']) + + if not config.exists(base + [vxlan, '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/nat/5-to-6 b/src/migration-scripts/nat/5-to-6 new file mode 100755 index 000000000..de3830582 --- /dev/null +++ b/src/migration-scripts/nat/5-to-6 @@ -0,0 +1,62 @@ +#!/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/>. + +# T5643: move from 'set nat [source|destination] rule X [inbound-interface|outbound interface] <iface>' +# to +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-name <iface>' + +from sys import argv,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() + +config = ConfigTree(config_file) + +if not config.exists(['nat']): + # Nothing to do + exit(0) + +for direction in ['source', 'destination']: + # If a node doesn't exist, we obviously have nothing to do. + if not config.exists(['nat', direction]): + continue + + # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, + # but there are no rules under it. + if not config.list_nodes(['nat', direction]): + continue + + for rule in config.list_nodes(['nat', direction, 'rule']): + base = ['nat', direction, 'rule', rule] + for iface in ['inbound-interface','outbound-interface']: + if config.exists(base + [iface]): + tmp = config.return_value(base + [iface]) + config.delete(base + [iface]) + config.set(base + [iface, 'interface-name'], value=tmp) + +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)) + exit(1) diff --git a/src/migration-scripts/nat/6-to-7 b/src/migration-scripts/nat/6-to-7 new file mode 100755 index 000000000..b5f6328ef --- /dev/null +++ b/src/migration-scripts/nat/6-to-7 @@ -0,0 +1,67 @@ +#!/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/>. + +# T5681: Firewall re-writing. Simplify cli when mathcing interface +# From +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-name <iface>' +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] interface-group <iface_group>' +# to +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] name <iface>' +# 'set nat [source|destination] rule X [inbound-interface|outbound interface] group <iface_group>' + +from sys import argv,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() + +config = ConfigTree(config_file) + +if not config.exists(['nat']): + # Nothing to do + exit(0) + +for direction in ['source', 'destination']: + # If a node doesn't exist, we obviously have nothing to do. + if not config.exists(['nat', direction]): + continue + + # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, + # but there are no rules under it. + if not config.list_nodes(['nat', direction]): + continue + + for rule in config.list_nodes(['nat', direction, 'rule']): + base = ['nat', direction, 'rule', rule] + for iface in ['inbound-interface','outbound-interface']: + if config.exists(base + [iface]): + if config.exists(base + [iface, 'interface-name']): + tmp = config.return_value(base + [iface, 'interface-name']) + config.delete(base + [iface, 'interface-name']) + config.set(base + [iface, 'name'], value=tmp) + +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)) + exit(1) diff --git a/src/migration-scripts/nat66/1-to-2 b/src/migration-scripts/nat66/1-to-2 new file mode 100755 index 000000000..b7d4e3f6b --- /dev/null +++ b/src/migration-scripts/nat66/1-to-2 @@ -0,0 +1,63 @@ +#!/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/>. + +# T5681: Firewall re-writing. Simplify cli when mathcing interface +# From +# 'set nat66 [source|destination] rule X [inbound-interface|outbound interface] <iface>' +# to +# 'set nat66 [source|destination] rule X [inbound-interface|outbound interface] name <iface>' + +from sys import argv,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() + +config = ConfigTree(config_file) +if not config.exists(['nat66']): + # Nothing to do + exit(0) + +for direction in ['source', 'destination']: + # If a node doesn't exist, we obviously have nothing to do. + if not config.exists(['nat66', direction]): + continue + + # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, + # but there are no rules under it. + if not config.list_nodes(['nat66', direction]): + continue + + for rule in config.list_nodes(['nat66', direction, 'rule']): + base = ['nat66', direction, 'rule', rule] + for iface in ['inbound-interface','outbound-interface']: + if config.exists(base + [iface]): + tmp = config.return_value(base + [iface]) + config.delete(base + [iface]) + config.set(base + [iface, 'name'], value=tmp) + +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)) + 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/migration-scripts/policy/6-to-7 b/src/migration-scripts/policy/6-to-7 new file mode 100755 index 000000000..727b8487a --- /dev/null +++ b/src/migration-scripts/policy/6-to-7 @@ -0,0 +1,79 @@ +#!/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/>. + +# T5729: Switch to valueless whenever is possible. +# From + # set policy [route | route6] ... rule <rule> log enable + # set policy [route | route6] ... rule <rule> log disable +# To + # set policy [route | route6] ... rule <rule> log + # Remove command if log=disable + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +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 = ['policy'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +for family in ['route', 'route6']: + if config.exists(base + [family]): + + for policy_name in config.list_nodes(base + [family]): + if config.exists(base + [family, policy_name, 'rule']): + for rule in config.list_nodes(base + [family, policy_name, 'rule']): + # Log + if config.exists(base + [family, policy_name, 'rule', rule, 'log']): + log_value = config.return_value(base + [family, policy_name, 'rule', rule, 'log']) + config.delete(base + [family, policy_name, 'rule', rule, 'log']) + if log_value == 'enable': + config.set(base + [family, policy_name, 'rule', rule, 'log']) + # State + if config.exists(base + [family, policy_name, 'rule', rule, 'state']): + flag_enable = 'False' + for state in ['established', 'invalid', 'new', 'related']: + if config.exists(base + [family, policy_name, 'rule', rule, 'state', state]): + state_value = config.return_value(base + [family, policy_name, 'rule', rule, 'state', state]) + config.delete(base + [family, policy_name, 'rule', rule, 'state', state]) + if state_value == 'enable': + config.set(base + [family, policy_name, 'rule', rule, 'state'], value=state, replace=False) + flag_enable = 'True' + if flag_enable == 'False': + config.delete(base + [family, policy_name, 'rule', rule, 'state']) + +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)) + exit(1)
\ No newline at end of file diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 3434707ec..20f54b9ba 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# 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 @@ -24,19 +24,28 @@ from vyos.config import Config from vyos.utils.process import cmd from vyos.utils.dict import dict_search_args -def get_config_firewall(conf, family=None, hook=None, priority=None): - config_path = ['firewall'] - if family: - config_path += [family] - if hook: - config_path += [hook] - if priority: - config_path += [priority] +def get_config_node(conf, node=None, family=None, hook=None, priority=None): + if node == 'nat': + if family == 'ipv6': + config_path = ['nat66'] + else: + config_path = ['nat'] - firewall = conf.get_config_dict(config_path, key_mangling=('-', '_'), + elif node == 'policy': + config_path = ['policy'] + else: + config_path = ['firewall'] + if family: + config_path += [family] + if hook: + config_path += [hook] + if priority: + config_path += [priority] + + node_config = conf.get_config_dict(config_path, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - return firewall + return node_config def get_nftables_details(family, hook, priority): if family == 'ipv6': @@ -102,7 +111,15 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N row.append(rule_details['conditions']) rows.append(row) - if 'default_action' in firewall_conf and not single_rule_id: + if hook in ['input', 'forward', 'output']: + def_action = firewall_conf['default_action'] if 'default_action' in firewall_conf else 'accept' + row = ['default', def_action, 'all'] + rule_details = details['default-action'] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) + rows.append(row) + + elif 'default_action' in firewall_conf and not single_rule_id: row = ['default', firewall_conf['default_action'], 'all'] if 'default-action' in details: rule_details = details['default-action'] @@ -167,16 +184,16 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule dest_addr = 'any' # Get inbound interface - iiface = dict_search_args(rule_conf, 'inbound_interface', 'interface_name') + iiface = dict_search_args(rule_conf, 'inbound_interface', 'name') if not iiface: - iiface = dict_search_args(rule_conf, 'inbound_interface', 'interface_group') + iiface = dict_search_args(rule_conf, 'inbound_interface', 'group') if not iiface: iiface = 'any' # Get outbound interface - oiface = dict_search_args(rule_conf, 'outbound_interface', 'interface_name') + oiface = dict_search_args(rule_conf, 'outbound_interface', 'name') if not oiface: - oiface = dict_search_args(rule_conf, 'outbound_interface', 'interface_group') + oiface = dict_search_args(rule_conf, 'outbound_interface', 'group') if not oiface: oiface = 'any' @@ -198,8 +215,9 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule if hook in ['input', 'forward', 'output']: row = ['default'] - row.append('N/A') - row.append('N/A') + rule_details = details['default-action'] + row.append(rule_details.get('packets', 0)) + row.append(rule_details.get('bytes', 0)) if 'default_action' in prior_conf: row.append(prior_conf['default_action']) else: @@ -234,7 +252,7 @@ def show_firewall(): print('Rulesets Information') conf = Config() - firewall = get_config_firewall(conf) + firewall = get_config_node(conf) if not firewall: return @@ -249,7 +267,7 @@ def show_firewall_family(family): print(f'Rulesets {family} Information') conf = Config() - firewall = get_config_firewall(conf) + firewall = get_config_node(conf) if not firewall or family not in firewall: return @@ -262,7 +280,7 @@ def show_firewall_name(family, hook, priority): print('Ruleset Information') conf = Config() - firewall = get_config_firewall(conf, family, hook, priority) + firewall = get_config_node(conf, 'firewall', family, hook, priority) if firewall: output_firewall_name(family, hook, priority, firewall) @@ -270,17 +288,20 @@ def show_firewall_rule(family, hook, priority, rule_id): print('Rule Information') conf = Config() - firewall = get_config_firewall(conf, family, hook, priority) + firewall = get_config_node(conf, 'firewall', family, hook, priority) if firewall: output_firewall_name(family, hook, priority, firewall, rule_id) def show_firewall_group(name=None): conf = Config() - firewall = get_config_firewall(conf) + firewall = get_config_node(conf, node='firewall') if 'group' not in firewall: return + nat = get_config_node(conf, node='nat') + policy = get_config_node(conf, node='policy') + def find_references(group_type, group_name): out = [] family = [] @@ -296,6 +317,7 @@ def show_firewall_group(name=None): family = ['ipv4', 'ipv6'] for item in family: + # Look references in firewall for name_type in ['name', 'ipv6_name', 'forward', 'input', 'output']: if item in firewall: if name_type not in firewall[item]: @@ -308,8 +330,8 @@ def show_firewall_group(name=None): for rule_id, rule_conf in priority_conf['rule'].items(): source_group = dict_search_args(rule_conf, 'source', 'group', group_type) dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type) - in_interface = dict_search_args(rule_conf, 'inbound_interface', 'interface_group') - out_interface = dict_search_args(rule_conf, 'outbound_interface', 'interface_group') + in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group') + out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group') if source_group: if source_group[0] == "!": source_group = source_group[1:] @@ -330,6 +352,76 @@ def show_firewall_group(name=None): out_interface = out_interface[1:] if group_name == out_interface: out.append(f'{item}-{name_type}-{priority}-{rule_id}') + + # Look references in route | route6 + for name_type in ['route', 'route6']: + if name_type not in policy: + continue + if name_type == 'route' and item == 'ipv6': + continue + elif name_type == 'route6' and item == 'ipv4': + continue + else: + for policy_name, policy_conf in policy[name_type].items(): + if 'rule' not in policy_conf: + continue + for rule_id, rule_conf in policy_conf['rule'].items(): + source_group = dict_search_args(rule_conf, 'source', 'group', group_type) + dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type) + in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group') + out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group') + if source_group: + if source_group[0] == "!": + source_group = source_group[1:] + if group_name == source_group: + out.append(f'{name_type}-{policy_name}-{rule_id}') + if dest_group: + if dest_group[0] == "!": + dest_group = dest_group[1:] + if group_name == dest_group: + out.append(f'{name_type}-{policy_name}-{rule_id}') + if in_interface: + if in_interface[0] == "!": + in_interface = in_interface[1:] + if group_name == in_interface: + out.append(f'{name_type}-{policy_name}-{rule_id}') + if out_interface: + if out_interface[0] == "!": + out_interface = out_interface[1:] + if group_name == out_interface: + out.append(f'{name_type}-{policy_name}-{rule_id}') + + ## Look references in nat table + for direction in ['source', 'destination']: + if direction in nat: + if 'rule' not in nat[direction]: + continue + for rule_id, rule_conf in nat[direction]['rule'].items(): + source_group = dict_search_args(rule_conf, 'source', 'group', group_type) + dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type) + in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group') + out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group') + if source_group: + if source_group[0] == "!": + source_group = source_group[1:] + if group_name == source_group: + out.append(f'nat-{direction}-{rule_id}') + if dest_group: + if dest_group[0] == "!": + dest_group = dest_group[1:] + if group_name == dest_group: + out.append(f'nat-{direction}-{rule_id}') + if in_interface: + if in_interface[0] == "!": + in_interface = in_interface[1:] + if group_name == in_interface: + out.append(f'nat-{direction}-{rule_id}') + if out_interface: + if out_interface[0] == "!": + out_interface = out_interface[1:] + if group_name == out_interface: + out.append(f'nat-{direction}-{rule_id}') + return out header = ['Name', 'Type', 'References', 'Members'] @@ -356,6 +448,7 @@ def show_firewall_group(name=None): row.append('N/D') rows.append(row) + if rows: print('Firewall Groups\n') print(tabulate.tabulate(rows, header)) @@ -364,7 +457,7 @@ def show_summary(): print('Ruleset Summary') conf = Config() - firewall = get_config_firewall(conf) + firewall = get_config_node(conf) if not firewall: return @@ -410,7 +503,7 @@ def show_statistics(): print('Rulesets Statistics') conf = Config() - firewall = get_config_firewall(conf) + firewall = get_config_node(conf) if not firewall: return 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/mtr.py b/src/op_mode/mtr.py new file mode 100644 index 000000000..de139f2fa --- /dev/null +++ b/src/op_mode/mtr.py @@ -0,0 +1,306 @@ +#! /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 sys +import socket +import ipaddress + +from vyos.utils.network import interface_list +from vyos.utils.network import vrf_list +from vyos.utils.process import call + +options = { + 'report': { + 'mtr': '{command} --report', + 'type': 'noarg', + 'help': 'This option puts mtr into report mode. When in this mode, mtr will run for the number of cycles specified by the -c option, and then print statistics and exit.' + }, + 'report-wide': { + 'mtr': '{command} --report-wide', + 'type': 'noarg', + 'help': 'This option puts mtr into wide report mode. When in this mode, mtr will not cut hostnames in the report.' + }, + 'raw': { + 'mtr': '{command} --raw', + 'type': 'noarg', + 'help': 'Use the raw output format. This format is better suited for archival of the measurement results.' + }, + 'json': { + 'mtr': '{command} --json', + 'type': 'noarg', + 'help': 'Use this option to tell mtr to use the JSON output format.' + }, + 'split': { + 'mtr': '{command} --split', + 'type': 'noarg', + 'help': 'Use this option to set mtr to spit out a format that is suitable for a split-user interface.' + }, + 'no-dns': { + 'mtr': '{command} --no-dns', + 'type': 'noarg', + 'help': 'Use this option to force mtr to display numeric IP numbers and not try to resolve the host names.' + }, + 'show-ips': { + 'mtr': '{command} --show-ips {value}', + 'type': '<num>', + 'help': 'Use this option to tell mtr to display both the host names and numeric IP numbers.' + }, + 'ipinfo': { + 'mtr': '{command} --ipinfo {value}', + 'type': '<num>', + 'help': 'Displays information about each IP hop.' + }, + 'aslookup': { + 'mtr': '{command} --aslookup', + 'type': 'noarg', + 'help': 'Displays the Autonomous System (AS) number alongside each hop. Equivalent to --ipinfo 0.' + }, + 'interval': { + 'mtr': '{command} --interval {value}', + 'type': '<num>', + 'help': 'Use this option to specify the positive number of seconds between ICMP ECHO requests. The default value for this parameter is one second. The root user may choose values between zero and one.' + }, + 'report-cycles': { + 'mtr': '{command} --report-cycles {value}', + 'type': '<num>', + 'help': 'Use this option to set the number of pings sent to determine both the machines on the network and the reliability of those machines. Each cycle lasts one second.' + }, + 'psize': { + 'mtr': '{command} --psize {value}', + 'type': '<num>', + 'help': 'This option sets the packet size used for probing. It is in bytes, inclusive IP and ICMP headers. If set to a negative number, every iteration will use a different, random packet size up to that number.' + }, + 'bitpattern': { + 'mtr': '{command} --bitpattern {value}', + 'type': '<num>', + 'help': 'Specifies bit pattern to use in payload. Should be within range 0 - 255. If NUM is greater than 255, a random pattern is used.' + }, + 'gracetime': { + 'mtr': '{command} --gracetime {value}', + 'type': '<num>', + 'help': 'Use this option to specify the positive number of seconds to wait for responses after the final request. The default value is five seconds.' + }, + 'tos': { + 'mtr': '{command} --tos {value}', + 'type': '<tos>', + 'help': 'Specifies value for type of service field in IP header. Should be within range 0 - 255.' + }, + 'mpls': { + 'mtr': '{command} --mpls {value}', + 'type': 'noarg', + 'help': 'Use this option to tell mtr to display information from ICMP extensions for MPLS (RFC 4950) that are encoded in the response packets.' + }, + 'interface': { + 'mtr': '{command} --interface {value}', + 'type': '<interface>', + 'helpfunction': interface_list, + 'help': 'Use the network interface with a specific name for sending network probes. This can be useful when you have multiple network interfaces with routes to your destination, for example both wired Ethernet and WiFi, and wish to test a particular interface.' + }, + 'address': { + 'mtr': '{command} --address {value}', + 'type': '<x.x.x.x> <h:h:h:h:h:h:h:h>', + 'help': 'Use this option to bind the outgoing socket to ADDRESS, so that all packets will be sent with ADDRESS as source address.' + }, + 'first-ttl': { + 'mtr': '{command} --first-ttl {value}', + 'type': '<num>', + 'help': 'Specifies with what TTL to start. Defaults to 1.' + }, + 'max-ttl': { + 'mtr': '{command} --max-ttl {value}', + 'type': '<num>', + 'help': 'Specifies the maximum number of hops or max time-to-live value mtr will probe. Default is 30.' + }, + 'max-unknown': { + 'mtr': '{command} --max-unknown {value}', + 'type': '<num>', + 'help': 'Specifies the maximum unknown host. Default is 5.' + }, + 'udp': { + 'mtr': '{command} --udp', + 'type': 'noarg', + 'help': 'Use UDP datagrams instead of ICMP ECHO.' + }, + 'tcp': { + 'mtr': '{command} --tcp', + 'type': 'noarg', + 'help': ' Use TCP SYN packets instead of ICMP ECHO. PACKETSIZE is ignored, since SYN packets can not contain data.' + }, + 'sctp': { + 'mtr': '{command} --sctp', + 'type': 'noarg', + 'help': 'Use Stream Control Transmission Protocol packets instead of ICMP ECHO.' + }, + 'port': { + 'mtr': '{command} --port {value}', + 'type': '<port>', + 'help': 'The target port number for TCP/SCTP/UDP traces.' + }, + 'localport': { + 'mtr': '{command} --localport {value}', + 'type': '<port>', + 'help': 'The source port number for UDP traces.' + }, + 'timeout': { + 'mtr': '{command} --timeout {value}', + 'type': '<num>', + 'help': ' The number of seconds to keep probe sockets open before giving up on the connection.' + }, + 'mark': { + 'mtr': '{command} --mark {value}', + 'type': '<num>', + 'help': ' Set the mark for each packet sent through this socket similar to the netfilter MARK target but socket-based. MARK is 32 unsigned integer.' + }, + 'vrf': { + 'mtr': 'sudo ip vrf exec {value} {command}', + 'type': '<vrf>', + 'help': 'Use specified VRF table', + 'helpfunction': vrf_list, + 'dflt': 'default' + } + } + +mtr = { + 4: '/bin/mtr -4', + 6: '/bin/mtr -6', +} + +class List(list): + def first(self): + return self.pop(0) if self else '' + + def last(self): + return self.pop() if self else '' + + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def expension_failure(option, completions): + reason = 'Ambiguous' if completions else 'Invalid' + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) + if completions: + sys.stderr.write(' Possible completions:\n ') + sys.stderr.write('\n '.join(completions)) + sys.stderr.write('\n') + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def complete(prefix): + return [o for o in options if o.startswith(prefix)] + + +def convert(command, args): + while args: + shortname = args.first() + longnames = complete(shortname) + if len(longnames) != 1: + expension_failure(shortname, longnames) + longname = longnames[0] + if options[longname]['type'] == 'noarg': + command = options[longname]['mtr'].format( + command=command, value='') + elif not args: + sys.exit(f'mtr: missing argument for {longname} option') + else: + command = options[longname]['mtr'].format( + command=command, value=args.first()) + return command + + +if __name__ == '__main__': + args = List(sys.argv[1:]) + host = args.first() + + if not host: + sys.exit("mtr: Missing host") + + + if host == '--get-options' or host == '--get-options-nested': + if host == '--get-options-nested': + args.first() # pop monitor + args.first() # pop mtr | traceroute + args.first() # pop IP + usedoptionslist = [] + while args: + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options + if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) + sys.stdout.write(' '.join(matched)) + sys.exit(0) + + if len(matched) > 1: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) + + value = args.first() # pop option's value + if not args: + matched = complete(option) + helplines = options[matched[0]]['type'] + # Run helpfunction to get list of possible values + if 'helpfunction' in options[matched[0]]: + result = options[matched[0]]['helpfunction']() + if result: + helplines = '\n' + ' '.join(result) + sys.stdout.write(helplines) + sys.exit(0) + + for name, option in options.items(): + if 'dflt' in option and name not in args: + args.append(name) + args.append(option['dflt']) + + try: + ip = socket.gethostbyname(host) + except UnicodeError: + sys.exit(f'mtr: Unknown host: {host}') + except socket.gaierror: + ip = host + + try: + version = ipaddress.ip_address(ip).version + except ValueError: + sys.exit(f'mtr: Unknown host: {host}') + + command = convert(mtr[version], args) + call(f'{command} --curses --displaymode 0 {host}') diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index f1d87a118..583d8792c 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -# Copyright (C) 2020 VyOS maintainers and contributors +# 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 @@ -14,29 +14,13 @@ # 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 sys import socket import ipaddress -from vyos.utils.network import get_all_vrfs -from vyos.ifconfig import Section - - -def interface_list() -> list: - """ - Get list of interfaces in system - :rtype: list - """ - return Section.interfaces() - - -def vrf_list() -> list: - """ - Get list of VRFs in system - :rtype: list - """ - return list(get_all_vrfs().keys()) +from vyos.utils.network import interface_list +from vyos.utils.network import vrf_list +from vyos.utils.process import call options = { 'audible': { @@ -295,6 +279,4 @@ if __name__ == '__main__': sys.exit(f'ping: Unknown host: {host}') command = convert(ping[version], args) - - # print(f'{command} {host}') - os.system(f'{command} {host}') + call(f'{command} {host}') 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/op_mode/ssh.py b/src/op_mode/ssh.py new file mode 100755 index 000000000..102becc55 --- /dev/null +++ b/src/op_mode/ssh.py @@ -0,0 +1,100 @@ +#!/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 json +import sys +import glob +import vyos.opmode +from vyos.utils.process import cmd +from vyos.configquery import ConfigTreeQuery +from tabulate import tabulate + +def show_fingerprints(raw: bool, ascii: bool): + config = ConfigTreeQuery() + if not config.exists("service ssh"): + raise vyos.opmode.UnconfiguredSubsystem("SSH server is not enabled.") + + publickeys = glob.glob("/etc/ssh/*.pub") + + if publickeys: + keys = [] + for keyfile in publickeys: + try: + if ascii: + keydata = cmd("ssh-keygen -l -v -E sha256 -f " + keyfile).splitlines() + else: + keydata = cmd("ssh-keygen -l -E sha256 -f " + keyfile).splitlines() + type = keydata[0].split(None)[-1].strip("()") + key_size = keydata[0].split(None)[0] + fingerprint = keydata[0].split(None)[1] + comment = keydata[0].split(None)[2:-1][0] + if ascii: + ascii_art = "\n".join(keydata[1:]) + keys.append({"type": type, "key_size": key_size, "fingerprint": fingerprint, "comment": comment, "ascii_art": ascii_art}) + else: + keys.append({"type": type, "key_size": key_size, "fingerprint": fingerprint, "comment": comment}) + except: + # Ignore invalid public keys + pass + if raw: + return keys + else: + headers = {"type": "Type", "key_size": "Key Size", "fingerprint": "Fingerprint", "comment": "Comment", "ascii_art": "ASCII Art"} + output = "SSH server public key fingerprints:\n\n" + tabulate(keys, headers=headers, tablefmt="simple") + return output + else: + if raw: + return [] + else: + return "No SSH server public keys are found." + +def show_dynamic_protection(raw: bool): + config = ConfigTreeQuery() + if not config.exists(['service', 'ssh', 'dynamic-protection']): + raise vyos.opmode.UnconfiguredSubsystem("SSH server dynamic-protection is not enabled.") + + attackers = [] + try: + # IPv4 + attackers = attackers + json.loads(cmd("nft -j list set ip sshguard attackers"))["nftables"][1]["set"]["elem"] + except: + pass + try: + # IPv6 + attackers = attackers + json.loads(cmd("nft -j list set ip6 sshguard attackers"))["nftables"][1]["set"]["elem"] + except: + pass + if attackers: + if raw: + return attackers + else: + output = "Blocked attackers:\n" + "\n".join(attackers) + return output + else: + if raw: + return [] + else: + return "No blocked attackers." + +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/traceroute.py b/src/op_mode/traceroute.py index 2f0edf53a..d2bac3f7c 100755 --- a/src/op_mode/traceroute.py +++ b/src/op_mode/traceroute.py @@ -14,29 +14,13 @@ # 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 sys import socket import ipaddress -from vyos.utils.network import get_all_vrfs -from vyos.ifconfig import Section - - -def interface_list() -> list: - """ - Get list of interfaces in system - :rtype: list - """ - return Section.interfaces() - - -def vrf_list() -> list: - """ - Get list of VRFs in system - :rtype: list - """ - return list(get_all_vrfs().keys()) +from vyos.utils.network import interface_list +from vyos.utils.network import vrf_list +from vyos.utils.process import call options = { 'backward-hops': { @@ -251,6 +235,4 @@ if __name__ == '__main__': sys.exit(f'traceroute: Unknown host: {host}') command = convert(traceroute[version], args) - - # print(f'{command} {host}') - os.system(f'{command} {host}') + call(f'{command} {host}') 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() diff --git a/src/validators/accel-radius-dictionary b/src/validators/accel-radius-dictionary deleted file mode 100755 index 05287e770..000000000 --- a/src/validators/accel-radius-dictionary +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -DICT_PATH=/usr/share/accel-ppp/radius -NAME=$1 - -if [ -n "$NAME" -a -e $DICT_PATH/dictionary.$NAME ]; then - exit 0 -else - echo "$NAME is not a valid RADIUS dictionary name" - echo "Please make sure that $DICT_PATH/dictionary.$NAME file exists" - exit 1 -fi - diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index bc6826120..8f455e12e 100755 --- a/src/validators/ddclient-protocol +++ b/src/validators/ddclient-protocol @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -ddclient -list-protocols | grep -vE 'nsupdate|cloudns' | grep -qw $1 +ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" |