diff options
263 files changed, 6875 insertions, 1176 deletions
@@ -6,6 +6,7 @@ SHIM_DIR := src/shim XDP_DIR := src/xdp LIBS := -lzmq CFLAGS := +BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH) J2LINT := $(shell command -v j2lint 2> /dev/null) @@ -37,10 +38,20 @@ interface_definitions: $(config_xml_obj) rm -rf $(TMPL_DIR)/qos rm -rf $(TMPL_DIR)/interfaces/input + # T2472 - EIGRP support + rm -rf $(TMPL_DIR)/protocols/eigrp + # T2773 - EIGRP support for VRF + rm -rf $(TMPL_DIR)/vrf/name/node.tag/protocols/eigrp + # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1' +ifeq ($(BUILD_ARCH),arm64) + # There is currently no telegraf support in VyOS for ARM64, remove CLI definitions + rm -rf $(TMPL_DIR)/service/monitoring/telegraf +endif + .PHONY: op_mode_definitions .ONESHELL: op_mode_definitions: $(op_xml_obj) diff --git a/data/templates/accel-ppp/config_shaper_radius.j2 b/data/templates/accel-ppp/config_shaper_radius.j2 index c256647e4..942cdf132 100644 --- a/data/templates/accel-ppp/config_shaper_radius.j2 +++ b/data/templates/accel-ppp/config_shaper_radius.j2 @@ -6,5 +6,8 @@ attr={{ authentication.radius.rate_limit.attribute }} {% if authentication.radius.rate_limit.vendor is vyos_defined %} vendor={{ authentication.radius.rate_limit.vendor }} {% endif %} +{% if authentication.radius.rate_limit.multiplier is vyos_defined %} +rate-multiplier={{ authentication.radius.rate_limit.multiplier }} +{% endif %} {% endif %} {% endif %} diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 3c0d47b27..6df12db2c 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -128,10 +128,16 @@ bind={{ radius_source_address }} {% if radius_dynamic_author %} dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} {% endif %} -{% if radius_shaper_attr %} + +{% if radius_shaper_enable %} [shaper] verbose=1 +{% if radius_shaper_attr %} attr={{ radius_shaper_attr }} +{% endif %} +{% if radius_shaper_multiplier %} +rate-multiplier={{ radius_shaper_multiplier }} +{% endif %} {% if radius_shaper_vendor %} vendor={{ radius_shaper_vendor }} {% endif %} diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index 5c6f19306..7ee28dd21 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -28,6 +28,7 @@ disable [sstp] verbose=1 ifname=sstp%d +port={{ port }} accept=ssl ssl-ca-file=/run/accel-pppd/sstp-ca.pem ssl-pemfile=/run/accel-pppd/sstp-cert.pem diff --git a/data/templates/container/registries.conf.j2 b/data/templates/container/registries.conf.j2 index 6a3be58d0..2e86466a1 100644 --- a/data/templates/container/registries.conf.j2 +++ b/data/templates/container/registries.conf.j2 @@ -1,4 +1,4 @@ -### Autogenerated by /usr/libexec/vyos/conf_mode/container.py ### +### Autogenerated by container.py ### # For more information on this configuration file, see containers-registries.conf(5). # diff --git a/data/templates/container/storage.conf.j2 b/data/templates/container/storage.conf.j2 index 97e1a9d0c..665f9bf95 100644 --- a/data/templates/container/storage.conf.j2 +++ b/data/templates/container/storage.conf.j2 @@ -1,5 +1,4 @@ -### Autogenerated by /usr/libexec/vyos/conf_mode/container.py ### - +### Autogenerated by container.py ### [storage] driver = "vfs" - graphroot = "/config/containers/storage" + graphroot = "/usr/lib/live/mount/persistence/container/storage" diff --git a/data/templates/dns-forwarding/recursor.conf.j2 b/data/templates/dns-forwarding/recursor.conf.j2 index c1950e1bc..ce1b676d1 100644 --- a/data/templates/dns-forwarding/recursor.conf.j2 +++ b/data/templates/dns-forwarding/recursor.conf.j2 @@ -32,6 +32,11 @@ local-address={{ listen_address | join(',') }} # dnssec dnssec={{ dnssec }} +{% if dns64_prefix is vyos_defined %} +# dns64-prefix +dns64-prefix={{ dns64_prefix }} +{% endif %} + # serve rfc1918 records serve-rfc1918={{ 'no' if no_serve_rfc1918 is vyos_defined else 'yes' }} diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index 4fa92f2e3..97fc123d5 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -1,32 +1,76 @@ +{% macro groups(group, is_ipv6) %} {% if group is vyos_defined %} -{% if group.address_group is vyos_defined %} +{% set ip_type = 'ipv6_addr' if is_ipv6 else 'ipv4_addr' %} +{% if group.address_group is vyos_defined and not is_ipv6 %} {% for group_name, group_conf in group.address_group.items() %} -define A_{{ group_name }} = { {{ group_conf.address | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set A_{{ group_name }} { + type {{ ip_type }} + flags interval +{% if group_conf.address is vyos_defined or includes %} + elements = { {{ group_conf.address | nft_nested_group(includes, group.address_group, 'address') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} -{% if group.ipv6_address_group is vyos_defined %} +{% if group.ipv6_address_group is vyos_defined and is_ipv6 %} {% for group_name, group_conf in group.ipv6_address_group.items() %} -define A6_{{ group_name }} = { {{ group_conf.address | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set A6_{{ group_name }} { + type {{ ip_type }} + flags interval +{% if group_conf.address is vyos_defined or includes %} + elements = { {{ group_conf.address | nft_nested_group(includes, group.ipv6_address_group, 'address') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} {% if group.mac_group is vyos_defined %} {% for group_name, group_conf in group.mac_group.items() %} -define M_{{ group_name }} = { {{ group_conf.mac_address | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set M_{{ group_name }} { + type ether_addr +{% if group_conf.mac_address is vyos_defined or includes %} + elements = { {{ group_conf.mac_address | nft_nested_group(includes, group.mac_group, 'mac_address') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} -{% if group.network_group is vyos_defined %} +{% if group.network_group is vyos_defined and not is_ipv6 %} {% for group_name, group_conf in group.network_group.items() %} -define N_{{ group_name }} = { {{ group_conf.network | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set N_{{ group_name }} { + type {{ ip_type }} + flags interval +{% if group_conf.network is vyos_defined or includes %} + elements = { {{ group_conf.network | nft_nested_group(includes, group.network_group, 'network') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} -{% if group.ipv6_network_group is vyos_defined %} +{% if group.ipv6_network_group is vyos_defined and is_ipv6 %} {% for group_name, group_conf in group.ipv6_network_group.items() %} -define N6_{{ group_name }} = { {{ group_conf.network | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set N6_{{ group_name }} { + type {{ ip_type }} + flags interval +{% if group_conf.network is vyos_defined or includes %} + elements = { {{ group_conf.network | nft_nested_group(includes, group.ipv6_network_group, 'network') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} {% if group.port_group is vyos_defined %} {% for group_name, group_conf in group.port_group.items() %} -define P_{{ group_name }} = { {{ group_conf.port | join(",") }} } +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set P_{{ group_name }} { + type inet_service + flags interval +{% if group_conf.port is vyos_defined or includes %} + elements = { {{ group_conf.port | nft_nested_group(includes, group.port_group, 'port') | join(",") }} } +{% endif %} + } {% endfor %} {% endif %} -{% endif %}
\ No newline at end of file +{% endif %} +{% endmacro %} diff --git a/data/templates/firewall/nftables-geoip-update.j2 b/data/templates/firewall/nftables-geoip-update.j2 new file mode 100644 index 000000000..f9e61a274 --- /dev/null +++ b/data/templates/firewall/nftables-geoip-update.j2 @@ -0,0 +1,33 @@ +#!/usr/sbin/nft -f + +{% if ipv4_sets is vyos_defined %} +{% for setname, ip_list in ipv4_sets.items() %} +flush set ip filter {{ setname }} +{% endfor %} + +table ip filter { +{% for setname, ip_list in ipv4_sets.items() %} + set {{ setname }} { + type ipv4_addr + flags interval + elements = { {{ ','.join(ip_list) }} } + } +{% endfor %} +} +{% endif %} + +{% if ipv6_sets is vyos_defined %} +{% for setname, ip_list in ipv6_sets.items() %} +flush set ip6 filter {{ setname }} +{% endfor %} + +table ip6 filter { +{% for setname, ip_list in ipv6_sets.items() %} + set {{ setname }} { + type ipv6_addr + flags interval + elements = { {{ ','.join(ip_list) }} } + } +{% endfor %} +} +{% endif %} diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2 index 0154c9f7e..281525407 100644 --- a/data/templates/firewall/nftables-policy.j2 +++ b/data/templates/firewall/nftables-policy.j2 @@ -1,13 +1,13 @@ #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} + {% if cleanup_commands is vyos_defined %} {% for command in cleanup_commands %} {{ command }} {% endfor %} {% endif %} -include "/run/nftables_defines.conf" - table ip mangle { {% if first_install is vyos_defined %} chain VYOS_PBR_PREROUTING { @@ -29,6 +29,8 @@ table ip mangle { } {% endfor %} {% endif %} + +{{ group_tmpl.groups(firewall_group, False) }} } table ip6 mangle { @@ -52,4 +54,5 @@ table ip6 mangle { } {% endfor %} {% endif %} +{{ group_tmpl.groups(firewall_group, True) }} } diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index fac3fad03..b91fed615 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -1,13 +1,13 @@ #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} + {% if cleanup_commands is vyos_defined %} {% for command in cleanup_commands %} {{ command }} {% endfor %} {% endif %} -include "/run/nftables_defines.conf" - table ip filter { {% if first_install is vyos_defined %} chain VYOS_FW_FORWARD { @@ -45,6 +45,14 @@ table ip filter { {{ conf | nft_default_rule(name_text) }} } {% endfor %} +{% if group is vyos_defined and group.domain_group is vyos_defined %} +{% for name, name_config in group.domain_group.items() %} + set D_{{ name }} { + type ipv4_addr + flags interval + } +{% endfor %} +{% endif %} {% for set_name in ns.sets %} set RECENT_{{ set_name }} { type ipv4_addr @@ -52,7 +60,18 @@ table ip filter { flags dynamic } {% endfor %} +{% if geoip_updated.name is vyos_defined %} +{% for setname in geoip_updated.name %} + set {{ setname }} { + type ipv4_addr + flags interval + } +{% endfor %} +{% endif %} {% endif %} + +{{ group_tmpl.groups(group, False) }} + {% if state_policy is vyos_defined %} chain VYOS_STATE_POLICY { {% if state_policy.established is vyos_defined %} @@ -113,7 +132,18 @@ table ip6 filter { flags dynamic } {% endfor %} +{% if geoip_updated.ipv6_name is vyos_defined %} +{% for setname in geoip_updated.ipv6_name %} + set {{ setname }} { + type ipv6_addr + flags interval + } +{% endfor %} +{% endif %} {% endif %} + +{{ group_tmpl.groups(group, True) }} + {% if state_policy is vyos_defined %} chain VYOS_STATE_POLICY6 { {% if state_policy.established is vyos_defined %} diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl new file mode 100644 index 000000000..df98e74d6 --- /dev/null +++ b/data/templates/frr/daemons.frr.tmpl @@ -0,0 +1,54 @@ +zebra=yes +bgpd=yes +ospfd=yes +ospf6d=yes +ripd=yes +ripngd=yes +isisd=yes +pimd=no +ldpd=yes +nhrpd=no +eigrpd=yes +babeld=no +sharpd=no +pbrd=no +bfdd=yes +staticd=yes + +vtysh_enable=yes +zebra_options=" -s 90000000 --daemon -A 127.0.0.1 +{%- if irdp is defined %} -M irdp{% endif -%} +{%- if snmp is defined and snmp.zebra is defined %} -M snmp{% endif -%} +" +bgpd_options=" --daemon -A 127.0.0.1 +{%- if bmp is defined %} -M bmp{% endif -%} +{%- if snmp is defined and snmp.bgpd is defined %} -M snmp{% endif -%} +" +ospfd_options=" --daemon -A 127.0.0.1 +{%- if snmp is defined and snmp.ospfd is defined %} -M snmp{% endif -%} +" +ospf6d_options=" --daemon -A ::1 +{%- if snmp is defined and snmp.ospf6d is defined %} -M snmp{% endif -%} +" +ripd_options=" --daemon -A 127.0.0.1 +{%- if snmp is defined and snmp.ripd is defined %} -M snmp{% endif -%} +" +ripngd_options=" --daemon -A ::1" +isisd_options=" --daemon -A 127.0.0.1 +{%- if snmp is defined and snmp.isisd is defined %} -M snmp{% endif -%} +" +pimd_options=" --daemon -A 127.0.0.1" +ldpd_options=" --daemon -A 127.0.0.1 +{%- if snmp is defined and snmp.ldpd is defined %} -M snmp{% endif -%} +" +nhrpd_options=" --daemon -A 127.0.0.1" +eigrpd_options=" --daemon -A 127.0.0.1" +babeld_options=" --daemon -A 127.0.0.1" +sharpd_options=" --daemon -A 127.0.0.1" +pbrd_options=" --daemon -A 127.0.0.1" +staticd_options=" --daemon -A 127.0.0.1" +bfdd_options=" --daemon -A 127.0.0.1" + +watchfrr_enable=no +valgrind_enable=no + diff --git a/data/templates/frr/eigrpd.frr.j2 b/data/templates/frr/eigrpd.frr.j2 new file mode 100644 index 000000000..67f8a3ad1 --- /dev/null +++ b/data/templates/frr/eigrpd.frr.j2 @@ -0,0 +1,21 @@ +!
+router eigrp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}
+{% if maximum_paths is vyos_defined %}
+maximum-paths {{ maximum_paths }}
+{% endif %}
+{% if metric.weights is vyos_defined %}
+metric weights {{ metric.weights }}
+{% endif %}
+{% if network is vyos_defined %}
+{% for net in network %}
+network {{ net }}
+{% endfor %}
+{% endif %}
+{% if redistribute is vyos_defined %}
+{% for protocol in redistribute %}
+redistribute {{ protocol }}
+{% endfor %}
+{% endif %}
+{% if variance is vyos_defined %}
+variance {{ variance }}
+{% endif %}
\ No newline at end of file diff --git a/data/templates/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2 index f0a64cb89..33df17770 100644 --- a/data/templates/frr/policy.frr.j2 +++ b/data/templates/frr/policy.frr.j2 @@ -185,12 +185,24 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.match.ip.address.prefix_list is vyos_defined %} match ip address prefix-list {{ rule_config.match.ip.address.prefix_list }} {% endif %} +{% if rule_config.match.ip.address.prefix_len is vyos_defined %} + match ip address prefix-len {{ rule_config.match.ip.address.prefix_len }} +{% endif %} {% if rule_config.match.ip.nexthop.access_list is vyos_defined %} match ip next-hop {{ rule_config.match.ip.nexthop.access_list }} {% endif %} +{% if rule_config.match.ip.nexthop.address is vyos_defined %} + match ip next-hop address {{ rule_config.match.ip.nexthop.address }} +{% endif %} +{% if rule_config.match.ip.nexthop.prefix_len is vyos_defined %} + match ip next-hop prefix-len {{ rule_config.match.ip.nexthop.prefix_len }} +{% endif %} {% if rule_config.match.ip.nexthop.prefix_list is vyos_defined %} match ip next-hop prefix-list {{ rule_config.match.ip.nexthop.prefix_list }} {% endif %} +{% if rule_config.match.ip.nexthop.type is vyos_defined %} + match ip next-hop type {{ rule_config.match.ip.nexthop.type }} +{% endif %} {% if rule_config.match.ip.route_source.access_list is vyos_defined %} match ip route-source {{ rule_config.match.ip.route_source.access_list }} {% endif %} @@ -203,8 +215,20 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.match.ipv6.address.prefix_list is vyos_defined %} match ipv6 address prefix-list {{ rule_config.match.ipv6.address.prefix_list }} {% endif %} -{% if rule_config.match.ipv6.nexthop is vyos_defined %} - match ipv6 next-hop address {{ rule_config.match.ipv6.nexthop }} +{% if rule_config.match.ipv6.address.prefix_len is vyos_defined %} + match ipv6 address prefix-len {{ rule_config.match.ipv6.address.prefix_len }} +{% endif %} +{% if rule_config.match.ipv6.nexthop.address is vyos_defined %} + match ipv6 next-hop address {{ rule_config.match.ipv6.nexthop.address }} +{% endif %} +{% if rule_config.match.ipv6.nexthop.access_list is vyos_defined %} + match ipv6 next-hop {{ rule_config.match.ipv6.nexthop.access_list }} +{% endif %} +{% if rule_config.match.ipv6.nexthop.prefix_list is vyos_defined %} + match ipv6 next-hop prefix-list {{ rule_config.match.ipv6.nexthop.prefix_list }} +{% endif %} +{% if rule_config.match.ipv6.nexthop.type is vyos_defined %} + match ipv6 next-hop type {{ rule_config.match.ipv6.nexthop.type }} {% endif %} {% if rule_config.match.large_community.large_community_list is vyos_defined %} match large-community {{ rule_config.match.large_community.large_community_list }} @@ -259,6 +283,12 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.set.distance is vyos_defined %} set distance {{ rule_config.set.distance }} {% endif %} +{% if rule_config.set.evpn.gateway.ipv4 is vyos_defined %} + set evpn gateway-ip ipv4 {{ rule_config.set.evpn.gateway.ipv4 }} +{% endif %} +{% if rule_config.set.evpn.gateway.ipv6 is vyos_defined %} + set evpn gateway-ip ipv6 {{ rule_config.set.evpn.gateway.ipv6 }} +{% endif %} {% if rule_config.set.extcommunity.bandwidth is vyos_defined %} set extcommunity bandwidth {{ rule_config.set.extcommunity.bandwidth }} {% endif %} diff --git a/data/templates/frr/ripd.frr.j2 b/data/templates/frr/ripd.frr.j2 index df35150ca..e9e484cc2 100644 --- a/data/templates/frr/ripd.frr.j2 +++ b/data/templates/frr/ripd.frr.j2 @@ -32,6 +32,12 @@ interface {{ iface }} {% if iface_config.split_horizon.poison_reverse is vyos_defined %} ip rip split-horizon poisoned-reverse {% endif %} +{% if iface_config.receive.version is vyos_defined %} + ip rip receive version {{ iface_config.receive.version }} +{% endif %} +{% if iface_config.send.version is vyos_defined %} + ip rip send version {{ iface_config.send.version }} +{% endif %} exit ! {% endfor %} @@ -84,6 +90,9 @@ router rip {% endif %} {% endif %} {% include 'frr/rip_ripng.frr.j2' %} +{% if version is vyos_defined %} + version {{ version }} +{% endif %} exit ! {% if route_map is vyos_defined %} diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2 index 589f03c2c..55c05ceb7 100644 --- a/data/templates/frr/staticd.frr.j2 +++ b/data/templates/frr/staticd.frr.j2 @@ -17,7 +17,7 @@ vrf {{ vrf }} {% endif %} {# IPv4 default routes from DHCP interfaces #} {% if dhcp is vyos_defined %} -{% for interface, interface_config in dhcp.items() %} +{% for interface, interface_config in dhcp.items() if interface_config.dhcp_options.no_default_route is not vyos_defined %} {% set next_hop = interface | get_dhcp_router %} {% if next_hop is vyos_defined %} {{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.dhcp_options.default_route_distance if interface_config.dhcp_options.default_route_distance is vyos_defined }} @@ -26,7 +26,7 @@ vrf {{ vrf }} {% endif %} {# IPv4 default routes from PPPoE interfaces #} {% if pppoe is vyos_defined %} -{% for interface, interface_config in pppoe.items() %} +{% for interface, interface_config in pppoe.items() if interface_config.no_default_route is not vyos_defined %} {{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210 {{ interface_config.default_route_distance if interface_config.default_route_distance is vyos_defined }} {% endfor %} {% endif %} diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index 70e62ae7a..dbb08e187 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -34,7 +34,7 @@ server { ssl_protocols TLSv1.2 TLSv1.3; # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) { + location ~ /(retrieve|configure|config-file|image|generate|show|reset|docs|openapi.json|redoc|graphql) { {% if server.api %} {% if server.api.socket %} proxy_pass http://unix:/run/api.sock; diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2 index 676ad88b3..d2760ec1f 100644 --- a/data/templates/ipsec/swanctl/remote_access.j2 +++ b/data/templates/ipsec/swanctl/remote_access.j2 @@ -18,7 +18,8 @@ {% endif %} local { {% if rw_conf.authentication.id is vyos_defined and rw_conf.authentication.use_x509_id is not vyos_defined %} - id = '{{ rw_conf.authentication.id }}' +{# please use " quotes - else Apple iOS goes crazy #} + id = "{{ rw_conf.authentication.id }}" {% endif %} {% if rw_conf.authentication.server_mode == 'x509' %} auth = pubkey diff --git a/data/templates/monitoring/override.conf.j2 b/data/templates/monitoring/override.conf.j2 index f8f150791..9f1b4ebec 100644 --- a/data/templates/monitoring/override.conf.j2 +++ b/data/templates/monitoring/override.conf.j2 @@ -2,6 +2,6 @@ After=vyos-router.service ConditionPathExists=/run/telegraf/vyos-telegraf.conf [Service] -Environment=INFLUX_TOKEN={{ authentication.token }} +Environment=INFLUX_TOKEN={{ influxdb.authentication.token }} CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN diff --git a/data/templates/monitoring/telegraf.j2 b/data/templates/monitoring/telegraf.j2 index d1a94366b..6b395692b 100644 --- a/data/templates/monitoring/telegraf.j2 +++ b/data/templates/monitoring/telegraf.j2 @@ -14,14 +14,31 @@ logfile = "" hostname = "" omit_hostname = false -{% if influxdb_configured is vyos_defined %} +{% if azure_data_explorer is vyos_defined %} +### Azure Data Explorer ### +[[outputs.azure_data_explorer]] + ## The URI property of the Azure Data Explorer resource on Azure + endpoint_url = "{{ azure_data_explorer.url }}" + + ## The Azure Data Explorer database that the metrics will be ingested into. + ## The plugin will NOT generate this database automatically, it's expected that this database already exists before ingestion. + database = "{{ azure_data_explorer.database }}" + metrics_grouping_type = "{{ azure_data_explorer.group_metrics }}" + + ## Name of the single table to store all the metrics (Only needed if metrics_grouping_type is "SingleTable"). +{% if azure_data_explorer.table is vyos_defined and azure_data_explorer.group_metrics == 'SingleTable' %} + table_name = "{{ azure_data_explorer.table }}" +{% endif %} +### End Azure Data Explorer ### +{% endif %} +{% if influxdb is vyos_defined %} ### InfluxDB2 ### [[outputs.influxdb_v2]] - urls = ["{{ url }}:{{ port }}"] + urls = ["{{ influxdb.url }}:{{ influxdb.port }}"] insecure_skip_verify = true token = "$INFLUX_TOKEN" - organization = "{{ authentication.organization }}" - bucket = "{{ bucket }}" + organization = "{{ influxdb.authentication.organization }}" + bucket = "{{ influxdb.bucket }}" ### End InfluxDB2 ### {% endif %} {% if prometheus_client is vyos_defined %} diff --git a/data/templates/ntp/ntpd.conf.j2 b/data/templates/ntp/ntpd.conf.j2 index da610051e..8921826fa 100644 --- a/data/templates/ntp/ntpd.conf.j2 +++ b/data/templates/ntp/ntpd.conf.j2 @@ -33,10 +33,17 @@ restrict {{ address | address_from_cidr }} mask {{ address | netmask_from_cidr } {% endfor %} {% endif %} -{% if listen_address %} +{% if listen_address is vyos_defined or interface is vyos_defined %} # NTP should listen on configured addresses only interface ignore wildcard -{% for address in listen_address %} +{% if listen_address is vyos_defined %} +{% for address in listen_address %} interface listen {{ address }} -{% endfor %} +{% endfor %} +{% endif %} +{% if interface is vyos_defined %} +{% for ifname in interface %} +interface listen {{ ifname }} +{% endfor %} +{% endif %} {% endif %} diff --git a/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2 index ea6247005..a5016691f 100644 --- a/data/templates/pmacct/uacctd.conf.j2 +++ b/data/templates/pmacct/uacctd.conf.j2 @@ -21,12 +21,14 @@ imt_mem_pools_number: 169 {% set plugin = [] %} {% if netflow.server is vyos_defined %} {% for server in netflow.server %} -{% set _ = plugin.append('nfprobe[nf_' ~ server ~ ']') %} +{% set nf_server_key = 'nf_' ~ server | replace(':', '.') %} +{% set _ = plugin.append('nfprobe['~ nf_server_key ~ ']') %} {% endfor %} {% endif %} {% if sflow.server is vyos_defined %} {% for server in sflow.server %} -{% set _ = plugin.append('sfprobe[sf_' ~ server ~ ']') %} +{% set sf_server_key = 'sf_' ~ server | replace(':', '.') %} +{% set _ = plugin.append('sfprobe[' ~ sf_server_key ~ ']') %} {% endfor %} {% endif %} {% if disable_imt is not defined %} @@ -37,22 +39,24 @@ plugins: {{ plugin | join(',') }} {% if netflow.server is vyos_defined %} # NetFlow servers {% for server, server_config in netflow.server.items() %} -nfprobe_receiver[nf_{{ server }}]: {{ server }}:{{ server_config.port }} -nfprobe_version[nf_{{ server }}]: {{ netflow.version }} +{# # prevent pmacct syntax error when using IPv6 flow collectors #} +{% set nf_server_key = 'nf_' ~ server | replace(':', '.') %} +nfprobe_receiver[{{ nf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }} +nfprobe_version[{{ nf_server_key }}]: {{ netflow.version }} {% if netflow.engine_id is vyos_defined %} -nfprobe_engine[nf_{{ server }}]: {{ netflow.engine_id }} +nfprobe_engine[{{ nf_server_key }}]: {{ netflow.engine_id }} {% endif %} {% if netflow.max_flows is vyos_defined %} -nfprobe_maxflows[nf_{{ server }}]: {{ netflow.max_flows }} +nfprobe_maxflows[{{ nf_server_key }}]: {{ netflow.max_flows }} {% endif %} {% if netflow.sampling_rate is vyos_defined %} -sampling_rate[nf_{{ server }}]: {{ netflow.sampling_rate }} +sampling_rate[{{ nf_server_key }}]: {{ netflow.sampling_rate }} {% endif %} {% if netflow.source_address is vyos_defined %} -nfprobe_source_ip[nf_{{ server }}]: {{ netflow.source_address }} +nfprobe_source_ip[{{ nf_server_key }}]: {{ netflow.source_address }} {% endif %} {% if netflow.timeout is vyos_defined %} -nfprobe_timeouts[nf_{{ server }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }} +nfprobe_timeouts[{{ nf_server_key }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }} {% endif %} {% endfor %} @@ -61,13 +65,15 @@ nfprobe_timeouts[nf_{{ server }}]: expint={{ netflow.timeout.expiry_interval }}: {% if sflow.server is vyos_defined %} # sFlow servers {% for server, server_config in sflow.server.items() %} -sfprobe_receiver[sf_{{ server }}]: {{ server }}:{{ server_config.port }} -sfprobe_agentip[sf_{{ server }}]: {{ sflow.agent_address }} +{# # prevent pmacct syntax error when using IPv6 flow collectors #} +{% set sf_server_key = 'sf_' ~ server | replace(':', '.') %} +sfprobe_receiver[{{ sf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }} +sfprobe_agentip[{{ sf_server_key }}]: {{ sflow.agent_address }} {% if sflow.sampling_rate is vyos_defined %} -sampling_rate[sf_{{ server }}]: {{ sflow.sampling_rate }} +sampling_rate[{{ sf_server_key }}]: {{ sflow.sampling_rate }} {% endif %} {% if sflow.source_address is vyos_defined %} -sfprobe_source_ip[sf_{{ server }}]: {{ sflow.source_address }} +sfprobe_source_ip[{{ sf_server_key }}]: {{ sflow.source_address }} {% endif %} {% endfor %} diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index 6902dc05a..ed15b32f0 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -55,6 +55,9 @@ interface {{ iface }} { {% endif %} {% if iface_config.name_server is vyos_defined %} RDNSS {{ iface_config.name_server | join(" ") }} { +{% if iface_config.name_server_lifetime is vyos_defined %} + AdvRDNSSLifetime {{ iface_config.name_server_lifetime }}; +{% endif %} }; {% endif %} {% if iface_config.dnssl is vyos_defined %} diff --git a/data/templates/sla/owamp-override.conf.j2 b/data/templates/sla/owamp-override.conf.j2 new file mode 100644 index 000000000..b5ec161d4 --- /dev/null +++ b/data/templates/sla/owamp-override.conf.j2 @@ -0,0 +1,16 @@ +[Unit] +Description==OWAMP server +After=vyos-router.service +# Only start if there is a configuration file +ConditionFileNotEmpty=/etc/owamp-server/owamp-server.conf + +[Service] +KillMode=process +Type=simple +ExecStart=/usr/sbin/owampd -c /etc/owamp-server -R /var/run +ExecReload=/bin/kill -HUP $MAINPID +PIDFile=/run/owamp-server.pid +LimitNOFILE=4096 + +[Install] +WantedBy=multi-user.target diff --git a/data/templates/sla/owamp-server.conf.j2 b/data/templates/sla/owamp-server.conf.j2 new file mode 100644 index 000000000..6af963e57 --- /dev/null +++ b/data/templates/sla/owamp-server.conf.j2 @@ -0,0 +1,20 @@ +### Autogenerated by service_twamp-server.py ### + +user owamp +group owamp + +verbose +vardir /var/run + +# location for "recv" session files. +# The "catalog" subdirectory is completely cleaned and recreated each time +datadir /var/lib/owamp + +srcnode :{{ port }} + +# This is used to limit testing to a specific port range. The valid values are: +# 0 (twampd will let the system to pick the port number (ephemeral) +# low-high (A range. high must be larger than low.) +testports 8760-9960 + +diskfudge 3.0 diff --git a/data/templates/sla/twamp-override.conf.j2 b/data/templates/sla/twamp-override.conf.j2 new file mode 100644 index 000000000..34bbd228b --- /dev/null +++ b/data/templates/sla/twamp-override.conf.j2 @@ -0,0 +1,16 @@ +[Unit] +Description==TWAMP server +After=vyos-router.service +# Only start if there is a configuration file +ConditionFileNotEmpty=/etc/twamp-server/twamp-server.conf + +[Service] +KillMode=process +Type=simple +ExecStart=/usr/sbin/twampd -c /etc/twamp-server -R /var/run +ExecReload=/bin/kill -HUP $MAINPID +PIDFile=/run/twamp-server.pid +LimitNOFILE=4096 + +[Install] +WantedBy=multi-user.target diff --git a/data/templates/sla/twamp-server.conf.j2 b/data/templates/sla/twamp-server.conf.j2 new file mode 100644 index 000000000..ea5bbb54a --- /dev/null +++ b/data/templates/sla/twamp-server.conf.j2 @@ -0,0 +1,18 @@ +### Autogenerated by service_twamp-server.py ### + +user twamp +group twamp + +verbose +vardir /var/run + +# location for "recv" session files. +# The "catalog" subdirectory is completely cleaned and recreated each time +datadir /var/lib/twamp + +srcnode :{{ port }} + +# This is used to limit testing to a specific port range. The valid values are: +# 0 (twampd will let the system to pick the port number (ephemeral) +# low-high (A range. high must be larger than low.) +testports 18760-19960 diff --git a/data/templates/ssh/sshguard_config.j2 b/data/templates/ssh/sshguard_config.j2 new file mode 100644 index 000000000..58c6ad48d --- /dev/null +++ b/data/templates/ssh/sshguard_config.j2 @@ -0,0 +1,27 @@ +### Autogenerated by ssh.py ### + +{% if dynamic_protection is vyos_defined %} +# Full path to backend executable (required, no default) +BACKEND="/usr/libexec/sshguard/sshg-fw-nft-sets" + +# Shell command that provides logs on standard output. (optional, no default) +# Example 1: ssh and sendmail from systemd journal: +LOGREADER="LANG=C journalctl -afb -p info -n1 -t sshd -o cat" + +#### OPTIONS #### +# Block attackers when their cumulative attack score exceeds THRESHOLD. +# Most attacks have a score of 10. (optional, default 30) +THRESHOLD={{ dynamic_protection.threshold }} + +# Block attackers for initially BLOCK_TIME seconds after exceeding THRESHOLD. +# Subsequent blocks increase by a factor of 1.5. (optional, default 120) +BLOCK_TIME={{ dynamic_protection.block_time }} + +# Remember potential attackers for up to DETECTION_TIME seconds before +# resetting their score. (optional, default 1800) +DETECTION_TIME={{ dynamic_protection.detect_time }} + +# IP addresses listed in the WHITELIST_FILE are considered to be +# friendlies and will never be blocked. +WHITELIST_FILE=/etc/sshguard/whitelist +{% endif %} diff --git a/data/templates/ssh/sshguard_whitelist.j2 b/data/templates/ssh/sshguard_whitelist.j2 new file mode 100644 index 000000000..47a950a2b --- /dev/null +++ b/data/templates/ssh/sshguard_whitelist.j2 @@ -0,0 +1,7 @@ +### Autogenerated by ssh.py ### + +{% if dynamic_protection.allow_from is vyos_defined %} +{% for address in dynamic_protection.allow_from %} +{{ address }} +{% endfor %} +{% endif %} diff --git a/data/templates/syslog/rsyslog.conf.j2 b/data/templates/syslog/rsyslog.conf.j2 index 4445d568b..abe880283 100644 --- a/data/templates/syslog/rsyslog.conf.j2 +++ b/data/templates/syslog/rsyslog.conf.j2 @@ -10,7 +10,11 @@ $MarkMessagePeriod {{ files['global']['marker-interval'] }} $PreserveFQDN on {% endif %} {% for file, file_options in files.items() %} +{% if file_options['max-size'] is vyos_defined %} $outchannel {{ file }},{{ file_options['log-file'] }},{{ file_options['max-size'] }},{{ file_options['action-on-max-size'] }} +{% else %} +$outchannel {{ file }},{{ file_options['log-file'] }} +{% endif %} {{ file_options['selectors'] }} :omfile:${{ file }} {% endfor %} {% if console is defined and console is not none %} diff --git a/data/templates/zone_policy/nftables.j2 b/data/templates/zone_policy/nftables.j2 index e4c4dd7da..fe941f9f8 100644 --- a/data/templates/zone_policy/nftables.j2 +++ b/data/templates/zone_policy/nftables.j2 @@ -16,7 +16,7 @@ table ip filter { iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }} iifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone_' + zone_name) }} } chain VZONE_{{ zone_name }}_OUT { oifname lo counter return @@ -24,7 +24,7 @@ table ip filter { oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }} oifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone_' + zone_name) }} } {% else %} chain VZONE_{{ zone_name }} { @@ -38,7 +38,7 @@ table ip filter { iifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endif %} {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone_' + zone_name) }} } {% endif %} {% endfor %} @@ -53,7 +53,7 @@ table ip6 filter { iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }} iifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} } chain VZONE6_{{ zone_name }}_OUT { oifname lo counter return @@ -61,7 +61,7 @@ table ip6 filter { oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }} oifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} } {% else %} chain VZONE6_{{ zone_name }} { @@ -75,7 +75,7 @@ table ip6 filter { iifname { {{ zone[from_zone].interface | join(",") }} } counter return {% endif %} {% endfor %} - counter {{ zone_conf.default_action }} + {{ zone_conf | nft_default_rule('zone6_' + zone_name) }} } {% endif %} {% endfor %} diff --git a/debian/control b/debian/control index c53e4d3b8..6a6ccf602 100644 --- a/debian/control +++ b/debian/control @@ -108,6 +108,8 @@ Depends: openvpn-auth-ldap, openvpn-auth-radius, openvpn-otp, + owamp-client, + owamp-server, pciutils, pdns-recursor, pmacct (>= 1.6.0), @@ -117,7 +119,6 @@ Depends: python3, python3-certbot-nginx, python3-cryptography, - python3-flask, python3-hurry.filesize, python3-inotify, python3-isc-dhcp-leases, @@ -133,7 +134,6 @@ Depends: python3-tabulate, python3-vici (>= 5.7.2), python3-voluptuous, - python3-waitress, python3-xmltodict, python3-zmq, qrencode, @@ -147,6 +147,7 @@ Depends: squid, squidclient, squidguard, + sshguard, ssl-cert, strongswan (>= 5.9), strongswan-swanctl (>= 5.9), @@ -159,6 +160,8 @@ Depends: tftpd-hpa, traceroute, tuned, + twamp-client, + twamp-server, udp-broadcast-relay, uidmap, usb-modeswitch, diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install index 3739763b9..406fef4be 100644 --- a/debian/vyos-1x-smoketest.install +++ b/debian/vyos-1x-smoketest.install @@ -1,4 +1,5 @@ usr/bin/vyos-smoketest usr/bin/vyos-configtest +usr/bin/vyos-configtest-pki usr/libexec/vyos/tests/smoke usr/libexec/vyos/tests/config diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 493c896eb..edd090993 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,4 +1,3 @@ -etc/cron.hourly etc/dhcp etc/ipsec.d etc/logrotate.d diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 85231b50c..51171d881 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -1,6 +1,6 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="container" owner="${vyos_conf_scripts_dir}/containers.py"> + <node name="container" owner="${vyos_conf_scripts_dir}/container.py"> <properties> <help>Container applications</help> <priority>1280</priority> diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 60e738e01..6e1592200 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -67,10 +67,7 @@ </node> <leafNode name="global-parameters"> <properties> - <help>Additional global parameters for DHCP server. You must - use the syntax of dhcpd.conf in this text-field. Using this - without proper knowledge may result in a crashed DHCP server. - Check system log to look for errors.</help> + <help>Additional global parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> @@ -111,10 +108,7 @@ #include <include/name-server-ipv4.xml.i> <leafNode name="shared-network-parameters"> <properties> - <help>Additional shared-network parameters for DHCP server. - You must use the syntax of dhcpd.conf in this text-field. - Using this without proper knowledge may result in a crashed - DHCP server. Check system log to look for errors.</help> + <help>Additional shared-network parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> @@ -134,17 +128,38 @@ <leafNode name="bootfile-name"> <properties> <help>Bootstrap file name</help> + <constraint> + <regex>[-_a-zA-Z0-9./]+</regex> + </constraint> </properties> </leafNode> <leafNode name="bootfile-server"> <properties> - <help>Server (IP address or domain name) from which the initial - boot file is to be loaded</help> + <help>Server from which the initial boot file is to be loaded</help> + <valueHelp> + <format>ipv4</format> + <description>Bootfile server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Bootfile server FQDN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="fqdn"/> + </constraint> </properties> </leafNode> <leafNode name="bootfile-size"> <properties> - <help>Bootstrap file size in 512 byte blocks</help> + <help>Bootstrap file size</help> + <valueHelp> + <format>u32:1-16</format> + <description>Bootstrap file size in 512 byte blocks</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16"/> + </constraint> </properties> </leafNode> <leafNode name="client-prefix-length"> @@ -326,11 +341,7 @@ </leafNode> <leafNode name="static-mapping-parameters"> <properties> - <help>Additional static-mapping parameters for DHCP server. - Will be placed inside the "host" block of the mapping. - You must use the syntax of dhcpd.conf in this text-field. - Using this without proper knowledge may result in a crashed - DHCP server. Check system log to look for errors.</help> + <help>Additional static-mapping parameters for DHCP server. Will be placed inside the "host" block of the mapping. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> @@ -338,7 +349,7 @@ </tagNode> <tagNode name="static-route"> <properties> - <help>Classless static route destination subnet [REQUIRED]</help> + <help>Classless static route destination subnet</help> <valueHelp> <format>ipv4net</format> <description>IPv4 address and prefix length</description> @@ -364,10 +375,7 @@ </tagNode > <leafNode name="subnet-parameters"> <properties> - <help>Additional subnet parameters for DHCP server. You must - use the syntax of dhcpd.conf in this text-field. Using this - without proper knowledge may result in a crashed DHCP server. - Check system log to look for errors.</help> + <help>Additional subnet parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index 10335b07e..9dff68a24 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -32,7 +32,7 @@ </leafNode> <tagNode name="shared-network-name"> <properties> - <help>DHCPv6 shared network name [REQUIRED]</help> + <help>DHCPv6 shared network name</help> <constraint> <regex>[-_a-zA-Z0-9.]+</regex> </constraint> @@ -64,7 +64,7 @@ </node> <tagNode name="subnet"> <properties> - <help>IPv6 DHCP subnet for this shared network [REQUIRED]</help> + <help>IPv6 DHCP subnet for this shared network</help> <valueHelp> <format>ipv6net</format> <description>IPv6 address and prefix length</description> diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index 0d6418272..70b2fb271 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -91,7 +91,7 @@ </leafNode> <leafNode name="inet"> <properties> - <help>IP Address [REQUIRED]</help> + <help>IP Address</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 6bc467b76..e41ba7f60 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -14,7 +14,7 @@ <children> <tagNode name="interface"> <properties> - <help>Interface to send DDNS updates for [REQUIRED]</help> + <help>Interface to send DDNS updates for</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> @@ -27,7 +27,7 @@ <children> <leafNode name="key"> <properties> - <help>File containing the secret key shared with remote DNS server [REQUIRED]</help> + <help>File containing the secret key shared with remote DNS server</help> <valueHelp> <format>filename</format> <description>File in /config/auth directory</description> @@ -36,13 +36,13 @@ </leafNode> <leafNode name="record"> <properties> - <help>Record to be updated [REQUIRED]</help> + <help>Record to be updated</help> <multi/> </properties> </leafNode> <leafNode name="server"> <properties> - <help>Server to be updated [REQUIRED]</help> + <help>Server to be updated</help> </properties> </leafNode> <leafNode name="ttl"> @@ -60,14 +60,14 @@ </leafNode> <leafNode name="zone"> <properties> - <help>Zone to be updated [REQUIRED]</help> + <help>Zone to be updated</help> </properties> </leafNode> </children> </tagNode> <tagNode name="service"> <properties> - <help>Service being used for Dynamic DNS [REQUIRED]</help> + <help>Service being used for Dynamic DNS</help> <completionHelp> <list>afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list> </completionHelp> @@ -127,23 +127,23 @@ <children> <leafNode name="host-name"> <properties> - <help>Hostname registered with DDNS service [REQUIRED]</help> + <help>Hostname registered with DDNS service</help> <multi/> </properties> </leafNode> <leafNode name="login"> <properties> - <help>Login for DDNS service [REQUIRED]</help> + <help>Login for DDNS service</help> </properties> </leafNode> <leafNode name="password"> <properties> - <help>Password for DDNS service [REQUIRED]</help> + <help>Password for DDNS service</help> </properties> </leafNode> <leafNode name="protocol"> <properties> - <help>ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM]</help> + <help>ddclient protocol used for DDNS service</help> <completionHelp> <list>changeip cloudflare dnsmadeeasy dnspark dondominio dslreports1 dtdns duckdns dyndns2 easydns freedns freemyip googledomains hammernode1 namecheap nfsn noip sitelutions woima yandex zoneedit1</list> </completionHelp> @@ -239,7 +239,7 @@ </leafNode> <leafNode name="server"> <properties> - <help>Server to send DDNS update to [REQUIRED FOR CUSTOM]</help> + <help>Server to send DDNS update to</help> <valueHelp> <format>IPv4</format> <description>IP address of DDNS server</description> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 6ead3e199..3de0dc0eb 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -36,6 +36,18 @@ <multi/> </properties> </leafNode> + <leafNode name="dns64-prefix"> + <properties> + <help>Help to communicate between IPv6-only client and IPv4-only server</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and /96 only prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> <leafNode name="dnssec"> <properties> <help>DNSSEC mode</help> @@ -133,14 +145,18 @@ <format>@</format> <description>Root record</description> </valueHelp> + <valueHelp> + <format>any</format> + <description>Wildcard record (any subdomain)</description> + </valueHelp> <constraint> - <regex>([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)</regex> + <regex>([-_a-zA-Z0-9.]{1,63}|@|any)(?<!\.)</regex> </constraint> </properties> <children> <leafNode name="address"> <properties> - <help>IPv4 address [REQUIRED]</help> + <help>IPv4 address</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> @@ -166,14 +182,18 @@ <format>@</format> <description>Root record</description> </valueHelp> + <valueHelp> + <format>any</format> + <description>Wildcard record (any subdomain)</description> + </valueHelp> <constraint> - <regex>([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)</regex> + <regex>([-_a-zA-Z0-9.]{1,63}|@|any)(?<!\.)</regex> </constraint> </properties> <children> <leafNode name="address"> <properties> - <help>IPv6 address [REQUIRED]</help> + <help>IPv6 address</help> <valueHelp> <format>ipv6</format> <description>IPv6 address</description> @@ -206,7 +226,7 @@ <children> <leafNode name="target"> <properties> - <help>Target DNS name [REQUIRED]</help> + <help>Target DNS name</help> <valueHelp> <format>name.example.com</format> <description>An absolute DNS name</description> @@ -238,7 +258,7 @@ <children> <tagNode name="server"> <properties> - <help>Mail server [REQUIRED]</help> + <help>Mail server</help> <valueHelp> <format>name.example.com</format> <description>An absolute DNS name</description> @@ -285,7 +305,7 @@ <children> <leafNode name="target"> <properties> - <help>Target DNS name [REQUIRED]</help> + <help>Target DNS name</help> <valueHelp> <format>name.example.com</format> <description>An absolute DNS name</description> @@ -317,7 +337,7 @@ <children> <leafNode name="value"> <properties> - <help>Record contents [REQUIRED]</help> + <help>Record contents</help> <valueHelp> <format>text</format> <description>Record contents</description> @@ -347,7 +367,7 @@ <children> <leafNode name="value"> <properties> - <help>Record contents [REQUIRED]</help> + <help>Record contents</help> <valueHelp> <format>text</format> <description>Record contents</description> @@ -376,7 +396,7 @@ <children> <tagNode name="entry"> <properties> - <help>Service entry [REQUIRED]</help> + <help>Service entry</help> <valueHelp> <format>u32:0-65535</format> <description>Entry number</description> @@ -388,7 +408,7 @@ <children> <leafNode name="hostname"> <properties> - <help>Server hostname [REQUIRED]</help> + <help>Server hostname</help> <valueHelp> <format>name.example.com</format> <description>An absolute DNS name</description> @@ -400,7 +420,7 @@ </leafNode> <leafNode name="port"> <properties> - <help>Port number [REQUIRED]</help> + <help>Port number</help> <valueHelp> <format>u32:0-65535</format> <description>TCP/UDP port number</description> @@ -460,7 +480,7 @@ <children> <tagNode name="rule"> <properties> - <help>NAPTR rule [REQUIRED]</help> + <help>NAPTR rule</help> <valueHelp> <format>u32:0-65535</format> <description>Rule number</description> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index ff8d92a24..2e9452dfd 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -97,6 +97,40 @@ <multi/> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another address-group</help> + <completionHelp> + <path>firewall group address-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> + #include <include/generic-description.xml.i> + </children> + </tagNode> + <tagNode name="domain-group"> + <properties> + <help>Firewall domain-group</help> + <constraint> + <regex>[a-zA-Z_][a-zA-Z0-9][\w\-\.]*</regex> + </constraint> + <constraintErrorMessage>Name of domain-group can only contain alpha-numeric letters, hyphen, underscores and not start with numeric</constraintErrorMessage> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>Domain-group member</help> + <valueHelp> + <format>txt</format> + <description>Domain address to match</description> + </valueHelp> + <constraint> + <regex>[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,99}?(\/.*)?</regex> + </constraint> + <multi/> + </properties> + </leafNode> #include <include/generic-description.xml.i> </children> </tagNode> @@ -126,6 +160,15 @@ <multi/> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another ipv6-address-group</help> + <completionHelp> + <path>firewall group ipv6-address-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> #include <include/generic-description.xml.i> </children> </tagNode> @@ -151,6 +194,15 @@ <multi/> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another ipv6-network-group</help> + <completionHelp> + <path>firewall group ipv6-network-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> </children> </tagNode> <tagNode name="mac-group"> @@ -175,6 +227,15 @@ <multi/> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another mac-group</help> + <completionHelp> + <path>firewall group mac-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> </children> </tagNode> <tagNode name="network-group"> @@ -199,6 +260,15 @@ <multi/> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another network-group</help> + <completionHelp> + <path>firewall group network-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> </children> </tagNode> <tagNode name="port-group"> @@ -231,6 +301,15 @@ </constraint> </properties> </leafNode> + <leafNode name="include"> + <properties> + <help>Include another port-group</help> + <completionHelp> + <path>firewall group port-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> </children> </tagNode> </children> @@ -287,6 +366,7 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> </children> @@ -297,6 +377,7 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> </children> @@ -473,6 +554,7 @@ </properties> <children> #include <include/firewall/address.xml.i> + #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> </children> @@ -483,6 +565,7 @@ </properties> <children> #include <include/firewall/address.xml.i> + #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> </children> @@ -520,6 +603,49 @@ #include <include/firewall/icmp-type-name.xml.i> </children> </node> + <node name="ttl"> + <properties> + <help>Time to live limit</help> + </properties> + <children> + <leafNode name="eq"> + <properties> + <help>Value to match a ttl equal to it</help> + <valueHelp> + <format>u32:0-255</format> + <description>ttl equal to value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="gt"> + <properties> + <help>Value to match a ttl greater than or equal to it</help> + <valueHelp> + <format>u32:0-255</format> + <description>ttl greater than value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lt"> + <properties> + <help>Value to match a ttl less than or equal to it</help> + <valueHelp> + <format>u32:0-255</format> + <description>ttl less than value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> </children> @@ -599,7 +725,7 @@ </properties> <children> #include <include/firewall/action-accept-drop-reject.xml.i> - #include <include/firewall/log.xml.i> + #include <include/firewall/rule-log-level.xml.i> </children> </node> <node name="invalid"> @@ -608,7 +734,7 @@ </properties> <children> #include <include/firewall/action-accept-drop-reject.xml.i> - #include <include/firewall/log.xml.i> + #include <include/firewall/rule-log-level.xml.i> </children> </node> <node name="related"> @@ -617,7 +743,7 @@ </properties> <children> #include <include/firewall/action-accept-drop-reject.xml.i> - #include <include/firewall/log.xml.i> + #include <include/firewall/rule-log-level.xml.i> </children> </node> </children> diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in index fc59f8ab3..878566b3f 100644 --- a/interface-definitions/flow-accounting-conf.xml.in +++ b/interface-definitions/flow-accounting-conf.xml.in @@ -220,7 +220,7 @@ </leafNode> <tagNode name="server"> <properties> - <help>Server to export NetFlow [REQUIRED]</help> + <help>NetFlow destination server</help> <valueHelp> <format>ipv4</format> <description>IPv4 server to export NetFlow</description> @@ -398,7 +398,7 @@ </leafNode> <tagNode name="server"> <properties> - <help>Server to export sFlow [REQUIRED]</help> + <help>sFlow destination server</help> <valueHelp> <format>ipv4</format> <description>IPv4 server to export sFlow</description> diff --git a/interface-definitions/igmp-proxy.xml.in b/interface-definitions/igmp-proxy.xml.in index 8e738fa7f..50cb33a93 100644 --- a/interface-definitions/igmp-proxy.xml.in +++ b/interface-definitions/igmp-proxy.xml.in @@ -18,7 +18,7 @@ </leafNode> <tagNode name="interface"> <properties> - <help>Interface for IGMP proxy [REQUIRED]</help> + <help>Interface for IGMP proxy</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> 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 be49fce5a..f44920c3f 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 @@ -21,6 +21,20 @@ <valueless /> </properties> </leafNode> + <leafNode name="multiplier"> + <properties> + <help>Shaper multiplier</help> + <valueHelp> + <format><0.001-1000></format> + <description>Shaper multiplier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0.001-1000 --float"/> + </constraint> + <constraintErrorMessage>Multiplier needs to be between 0.001 and 1000</constraintErrorMessage> + </properties> + <defaultValue>1</defaultValue> + </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/auth-local-users.xml.i b/interface-definitions/include/auth-local-users.xml.i index cb456eecf..9fb507474 100644 --- a/interface-definitions/include/auth-local-users.xml.i +++ b/interface-definitions/include/auth-local-users.xml.i @@ -19,74 +19,6 @@ <help>Password used for authentication</help> </properties> </leafNode> - <node name="otp"> - <properties> - <help>2FA OTP authentication parameters</help> - </properties> - <children> - <leafNode name="key"> - <properties> - <help>Token Key Secret key for the token algorithm (see RFC 4226)</help> - <valueHelp> - <format>txt</format> - <description>OTP key in hex-encoded format</description> - </valueHelp> - <constraint> - <regex>[a-fA-F0-9]{20,10000}</regex> - </constraint> - <constraintErrorMessage>Key name must only include hex characters and be at least 20 characters long</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="otp-length"> - <properties> - <help>Number of digits in OTP code</help> - <valueHelp> - <format>u32:6-8</format> - <description>Number of digits in OTP code</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 6-8"/> - </constraint> - <constraintErrorMessage>Number of digits in OTP code must be between 6 and 8</constraintErrorMessage> - </properties> - <defaultValue>6</defaultValue> - </leafNode> - <leafNode name="interval"> - <properties> - <help>Time tokens interval in seconds</help> - <valueHelp> - <format>u32:5-86400</format> - <description>Time tokens interval in seconds.</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 5-86400"/> - </constraint> - <constraintErrorMessage>Time token interval must be between 5 and 86400 seconds</constraintErrorMessage> - </properties> - <defaultValue>30</defaultValue> - </leafNode> - <leafNode name="token-type"> - <properties> - <help>Token type</help> - <valueHelp> - <format>hotp-time</format> - <description>Time-based OTP algorithm</description> - </valueHelp> - <valueHelp> - <format>hotp-event</format> - <description>Event-based OTP algorithm</description> - </valueHelp> - <constraint> - <regex>(hotp-time|hotp-event)</regex> - </constraint> - <completionHelp> - <list>hotp-time hotp-event</list> - </completionHelp> - </properties> - <defaultValue>hotp-time</defaultValue> - </leafNode> - </children> - </node> </children> </tagNode> </children> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index abaff5232..c1b465e43 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -1156,7 +1156,7 @@ <children> <leafNode name="identifier"> <properties> - <help>Confederation AS identifier [REQUIRED]</help> + <help>Confederation AS identifier</help> <valueHelp> <format>u32:1-4294967294</format> <description>Confederation AS id</description> @@ -1208,7 +1208,7 @@ <children> <leafNode name="half-life"> <properties> - <help>Half-life time for dampening [REQUIRED]</help> + <help>Half-life time for dampening</help> <valueHelp> <format>u32:1-45</format> <description>Half-life penalty in minutes</description> @@ -1220,7 +1220,7 @@ </leafNode> <leafNode name="max-suppress-time"> <properties> - <help>Maximum duration to suppress a stable route [REQUIRED]</help> + <help>Maximum duration to suppress a stable route</help> <valueHelp> <format>u32:1-255</format> <description>Maximum suppress duration in minutes</description> @@ -1232,7 +1232,7 @@ </leafNode> <leafNode name="re-use"> <properties> - <help>Threshold to start reusing a route [REQUIRED]</help> + <help>Threshold to start reusing a route</help> <valueHelp> <format>u32:1-20000</format> <description>Re-use penalty points</description> @@ -1244,7 +1244,7 @@ </leafNode> <leafNode name="start-suppress-time"> <properties> - <help>When to start suppressing a route [REQUIRED]</help> + <help>When to start suppressing a route</help> <valueHelp> <format>u32:1-20000</format> <description>Start-suppress penalty points</description> diff --git a/interface-definitions/include/bgp/remote-as.xml.i b/interface-definitions/include/bgp/remote-as.xml.i index 58595b3b9..79d3b95a9 100644 --- a/interface-definitions/include/bgp/remote-as.xml.i +++ b/interface-definitions/include/bgp/remote-as.xml.i @@ -1,7 +1,7 @@ <!-- include start from bgp/remote-as.xml.i --> <leafNode name="remote-as"> <properties> - <help>Neighbor BGP AS number [REQUIRED]</help> + <help>Neighbor BGP AS number</help> <completionHelp> <list>external internal</list> </completionHelp> diff --git a/interface-definitions/include/eigrp/protocol-common-config.xml.i b/interface-definitions/include/eigrp/protocol-common-config.xml.i new file mode 100644 index 000000000..147277102 --- /dev/null +++ b/interface-definitions/include/eigrp/protocol-common-config.xml.i @@ -0,0 +1,121 @@ +<!-- include start from eigrp/protocol-common-config.xml.i --> +<leafNode name="local-as"> + <properties> + <help>Autonomous System Number (ASN)</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Autonomous System Number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> +</leafNode> +<leafNode name="maximum-paths"> + <properties> + <help>Forward packets over multiple paths</help> + <valueHelp> + <format>u32:1-32</format> + <description>Number of paths</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-32"/> + </constraint> + </properties> +</leafNode> +<node name="metric"> + <properties> + <help>Modify metrics and parameters for advertisement</help> + </properties> + <children> + <leafNode name="weights"> + <properties> + <help>Modify metric coefficients</help> + <valueHelp> + <format>u32:0-255</format> + <description>K1</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<leafNode name="network"> + <properties> + <help>Enable routing on an IP network</help> + <valueHelp> + <format>ipv4net</format> + <description>EIGRP network prefix</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> +</leafNode> +<leafNode name="passive-interface"> + <properties> + <help>Suppress routing updates on an interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> +<leafNode name="redistribute"> + <properties> + <help>Redistribute information from another routing protocol</help> + <valueHelp> + <format>bgp</format> + <description>Border Gateway Protocol (BGP)</description> + </valueHelp> + <valueHelp> + <format>connected</format> + <description>Connected routes</description> + </valueHelp> + <valueHelp> + <format>nhrp</format> + <description>Next Hop Resolution Protocol (NHRP)</description> + </valueHelp> + <valueHelp> + <format>ospf</format> + <description>Open Shortest Path First (OSPFv2)</description> + </valueHelp> + <valueHelp> + <format>rip</format> + <description>Routing Information Protocol (RIP)</description> + </valueHelp> + <valueHelp> + <format>static</format> + <description>Statically configured routes</description> + </valueHelp> + <valueHelp> + <format>vnc</format> + <description>Virtual Network Control (VNC)</description> + </valueHelp> + <completionHelp> + <list>bgp connected nhrp ospf rip static vnc</list> + </completionHelp> + <constraint> + <regex>(bgp|connected|nhrp|ospf|rip|static|vnc)</regex> + </constraint> + <multi/> + </properties> +</leafNode> +#include <include/route-map.xml.i> +#include <include/router-id.xml.i> +<!-- FRR timers not implemented yet --> +<leafNode name="variance"> + <properties> + <help>Control load balancing variance</help> + <valueHelp> + <format>u32:1-128</format> + <description>Metric variance multiplier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-128"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/action.xml.i b/interface-definitions/include/firewall/action.xml.i index 0f60e3c38..512cc23bd 100644 --- a/interface-definitions/include/firewall/action.xml.i +++ b/interface-definitions/include/firewall/action.xml.i @@ -1,7 +1,7 @@ <!-- include start from firewall/action.xml.i --> <leafNode name="action"> <properties> - <help>Rule action [REQUIRED]</help> + <help>Rule action</help> <completionHelp> <list>accept reject drop</list> </completionHelp> diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i index cbdfa9dc2..079864122 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -95,6 +95,33 @@ </constraint> </properties> </leafNode> +#include <include/firewall/rule-log-level.xml.i> +<node name="connection-status"> + <properties> + <help>Connection status</help> + </properties> + <children> + <leafNode name="nat"> + <properties> + <help>NAT connection status</help> + <completionHelp> + <list>destination source</list> + </completionHelp> + <valueHelp> + <format>destination</format> + <description>Match connections that are subject to destination NAT</description> + </valueHelp> + <valueHelp> + <format>source</format> + <description>Match connections that are subject to source NAT</description> + </valueHelp> + <constraint> + <regex>^(destination|source)$</regex> + </constraint> + </properties> + </leafNode> + </children> +</node> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> diff --git a/interface-definitions/include/firewall/geoip.xml.i b/interface-definitions/include/firewall/geoip.xml.i new file mode 100644 index 000000000..9fb37a574 --- /dev/null +++ b/interface-definitions/include/firewall/geoip.xml.i @@ -0,0 +1,28 @@ +<!-- include start from firewall/geoip.xml.i --> +<node name="geoip"> + <properties> + <help>GeoIP options - Data provided by DB-IP.com</help> + </properties> + <children> + <leafNode name="country-code"> + <properties> + <help>GeoIP country code</help> + <valueHelp> + <format><country></format> + <description>Country code (2 characters)</description> + </valueHelp> + <constraint> + <regex>^(ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bl|bm|bn|bo|bq|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mf|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)$</regex> + </constraint> + <multi /> + </properties> + </leafNode> + <leafNode name="inverse-match"> + <properties> + <help>Inverse match of country-codes</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/firewall/name-default-log.xml.i b/interface-definitions/include/firewall/name-default-log.xml.i index 979395146..1d0ff9497 100644 --- a/interface-definitions/include/firewall/name-default-log.xml.i +++ b/interface-definitions/include/firewall/name-default-log.xml.i @@ -5,4 +5,4 @@ <valueless/> </properties> </leafNode> -<!-- include end --> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/rule-log-level.xml.i b/interface-definitions/include/firewall/rule-log-level.xml.i new file mode 100644 index 000000000..10c8de5e3 --- /dev/null +++ b/interface-definitions/include/firewall/rule-log-level.xml.i @@ -0,0 +1,45 @@ +<!-- include start from firewall/common-rule.xml.i --> +<leafNode name="log-level"> + <properties> + <help>Set log-level. Log must be enable.</help> + <completionHelp> + <list>emerg alert crit err warn notice info debug</list> + </completionHelp> + <valueHelp> + <format>emerg</format> + <description>Emerg log level</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Alert log level</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical log level</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error log level</description> + </valueHelp> + <valueHelp> + <format>warn</format> + <description>Warning log level</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Notice log level</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Info log level</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug log level</description> + </valueHelp> + <constraint> + <regex>(emerg|alert|crit|err|warn|notice|info|debug)</regex> + </constraint> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/source-destination-group.xml.i b/interface-definitions/include/firewall/source-destination-group.xml.i index ab11e89e9..6ebee356c 100644 --- a/interface-definitions/include/firewall/source-destination-group.xml.i +++ b/interface-definitions/include/firewall/source-destination-group.xml.i @@ -12,6 +12,14 @@ </completionHelp> </properties> </leafNode> + <leafNode name="domain-group"> + <properties> + <help>Group of domains</help> + <completionHelp> + <path>firewall group domain-group</path> + </completionHelp> + </properties> + </leafNode> #include <include/firewall/mac-group.xml.i> <leafNode name="network-group"> <properties> diff --git a/interface-definitions/include/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i index 44e87775c..65aae28ae 100644 --- a/interface-definitions/include/generic-interface-multi.xml.i +++ b/interface-definitions/include/generic-interface-multi.xml.i @@ -1,7 +1,7 @@ <!-- include start from generic-interface-multi.xml.i --> <leafNode name="interface"> <properties> - <help>Interface Name to use</help> + <help>Interface to use</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/include/generic-interface.xml.i b/interface-definitions/include/generic-interface.xml.i index 50af718a5..8b4cf1d65 100644 --- a/interface-definitions/include/generic-interface.xml.i +++ b/interface-definitions/include/generic-interface.xml.i @@ -1,7 +1,7 @@ <!-- include start from generic-interface.xml.i --> <leafNode name="interface"> <properties> - <help>Interface Name to use</help> + <help>Interface to use</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i index b9dd59bea..5057ed9ae 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6-dhcp.xml.i --> +<!-- include start from interface/address-ipv4-ipv6-dhcp.xml.i --> <leafNode name="address"> <properties> <help>IP address</help> diff --git a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i index 519622050..d689da5aa 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6.xml.i --> +<!-- include start from interface/address-ipv4-ipv6.xml.i --> <leafNode name="address"> <properties> <help>IP address</help> diff --git a/interface-definitions/include/interface/dhcpv6-options.xml.i b/interface-definitions/include/interface/dhcpv6-options.xml.i index 08e4f5e0a..c705af7c2 100644 --- a/interface-definitions/include/interface/dhcpv6-options.xml.i +++ b/interface-definitions/include/interface/dhcpv6-options.xml.i @@ -71,11 +71,11 @@ <properties> <help>Interface site-Level aggregator (SLA)</help> <valueHelp> - <format>u32:0-128</format> + <format>u32:0-65535</format> <description>Decimal integer which fits in the length of SLA IDs</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-128"/> + <validator name="numeric" argument="--range 0-65535"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/interface/enable-directed-broadcast.xml.i b/interface-definitions/include/interface/enable-directed-broadcast.xml.i new file mode 100644 index 000000000..a87395806 --- /dev/null +++ b/interface-definitions/include/interface/enable-directed-broadcast.xml.i @@ -0,0 +1,8 @@ +<!-- include start from interface/enable-directed-broadcast.xml.i --> +<leafNode name="enable-directed-broadcast"> + <properties> + <help>Enable directed broadcast forwarding on this interface</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/ipv4-options.xml.i b/interface-definitions/include/interface/ipv4-options.xml.i index bca1229c6..eda77e851 100644 --- a/interface-definitions/include/interface/ipv4-options.xml.i +++ b/interface-definitions/include/interface/ipv4-options.xml.i @@ -8,6 +8,7 @@ #include <include/interface/arp-cache-timeout.xml.i> #include <include/interface/disable-arp-filter.xml.i> #include <include/interface/disable-forwarding.xml.i> + #include <include/interface/enable-directed-broadcast.xml.i> #include <include/interface/enable-arp-accept.xml.i> #include <include/interface/enable-arp-announce.xml.i> #include <include/interface/enable-arp-ignore.xml.i> diff --git a/interface-definitions/include/ipsec/local-address.xml.i b/interface-definitions/include/ipsec/local-address.xml.i index dc5653ce7..9d267f3f7 100644 --- a/interface-definitions/include/ipsec/local-address.xml.i +++ b/interface-definitions/include/ipsec/local-address.xml.i @@ -4,6 +4,7 @@ <help>IPv4 or IPv6 address of a local interface to use for VPN</help> <completionHelp> <list>any</list> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> </completionHelp> <valueHelp> <format>ipv4</format> diff --git a/interface-definitions/include/monitoring/url.xml.i b/interface-definitions/include/monitoring/url.xml.i new file mode 100644 index 000000000..fd61c38ea --- /dev/null +++ b/interface-definitions/include/monitoring/url.xml.i @@ -0,0 +1,15 @@ +<!-- include start from monitoring/url.xml.i --> +<leafNode name="url"> + <properties> + <help>Remote URL</help> + <valueHelp> + <format>url</format> + <description>Remote URL</description> + </valueHelp> + <constraint> + <regex>(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?</regex> + </constraint> + <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i index c156d5b1c..791bbc0f8 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -16,7 +16,7 @@ <children> <leafNode name="export"> <properties> - <help>Filter for outgoing routing update [REQUIRED]</help> + <help>Filter for outgoing routing update</help> <completionHelp> <list>bgp connected kernel rip static</list> </completionHelp> @@ -178,10 +178,10 @@ </leafNode> <leafNode name="network"> <properties> - <help>OSPF network [REQUIRED]</help> + <help>OSPF network</help> <valueHelp> <format>ipv4net</format> - <description>OSPF network [REQUIRED]</description> + <description>OSPF network</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> diff --git a/interface-definitions/include/pki/ca-certificate-multi.xml.i b/interface-definitions/include/pki/ca-certificate-multi.xml.i new file mode 100644 index 000000000..646131b54 --- /dev/null +++ b/interface-definitions/include/pki/ca-certificate-multi.xml.i @@ -0,0 +1,15 @@ +<!-- include start from pki/ca-certificate-multi.xml.i --> +<leafNode name="ca-certificate"> + <properties> + <help>Certificate Authority chain in PKI configuration</help> + <completionHelp> + <path>pki ca</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Name of CA in PKI configuration</description> + </valueHelp> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/policy/action.xml.i b/interface-definitions/include/policy/action.xml.i index 0a3dc158a..5aa865523 100644 --- a/interface-definitions/include/policy/action.xml.i +++ b/interface-definitions/include/policy/action.xml.i @@ -1,7 +1,7 @@ <!-- include start from policy/action.xml.i --> <leafNode name="action"> <properties> - <help>Action to take on entries matching this rule [REQUIRED]</help> + <help>Action to take on entries matching this rule</help> <completionHelp> <list>permit deny</list> </completionHelp> diff --git a/interface-definitions/include/policy/route-rule-action.xml.i b/interface-definitions/include/policy/route-rule-action.xml.i index 1217055f2..456a21400 100644 --- a/interface-definitions/include/policy/route-rule-action.xml.i +++ b/interface-definitions/include/policy/route-rule-action.xml.i @@ -1,7 +1,7 @@ <!-- include start from policy/route-rule-action.xml.i --> <leafNode name="action"> <properties> - <help>Rule action [REQUIRED]</help> + <help>Rule action</help> <completionHelp> <list>drop</list> </completionHelp> diff --git a/interface-definitions/include/rip/rip-access-list.xml.i b/interface-definitions/include/rip/access-list.xml.i index 00ee9b736..8799aa9c3 100644 --- a/interface-definitions/include/rip/rip-access-list.xml.i +++ b/interface-definitions/include/rip/access-list.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-access-list.xml.i --> +<!-- include start from rip/access-list.xml.i --> <node name="access-list"> <properties> <help>Access-list</help> diff --git a/interface-definitions/include/rip/rip-access-list6.xml.i b/interface-definitions/include/rip/access-list6.xml.i index 9e4298bc0..732135253 100644 --- a/interface-definitions/include/rip/rip-access-list6.xml.i +++ b/interface-definitions/include/rip/access-list6.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-access-list.xml.i --> +<!-- include start from rip/access-list.xml.i --> <node name="access-list"> <properties> <help>Access-list</help> diff --git a/interface-definitions/include/rip/rip-default-information.xml.i b/interface-definitions/include/rip/default-information.xml.i index 28c540c26..957fb3a8d 100644 --- a/interface-definitions/include/rip/rip-default-information.xml.i +++ b/interface-definitions/include/rip/default-information.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-default-information.xml.i --> +<!-- include start from rip/default-information.xml.i --> <node name="default-information"> <properties> <help>Control distribution of default route</help> diff --git a/interface-definitions/include/rip/rip-default-metric.xml.i b/interface-definitions/include/rip/default-metric.xml.i index 297af5af8..c0f1f9b61 100644 --- a/interface-definitions/include/rip/rip-default-metric.xml.i +++ b/interface-definitions/include/rip/default-metric.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-default-metric.xml.i --> +<!-- include start from rip/default-metric.xml.i --> <leafNode name="default-metric"> <properties> <help>Metric of redistributed routes</help> diff --git a/interface-definitions/include/rip/rip-interface.xml.i b/interface-definitions/include/rip/interface.xml.i index dd3bddd4f..baeceac1c 100644 --- a/interface-definitions/include/rip/rip-interface.xml.i +++ b/interface-definitions/include/rip/interface.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-interface.xml.i --> +<!-- include start from rip/interface.xml.i --> <tagNode name="interface"> <properties> <help>Interface name</help> diff --git a/interface-definitions/include/rip/rip-prefix-list.xml.i b/interface-definitions/include/rip/prefix-list.xml.i index 2569a2a09..8e806aa35 100644 --- a/interface-definitions/include/rip/rip-prefix-list.xml.i +++ b/interface-definitions/include/rip/prefix-list.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-prefix-list.xml.i --> +<!-- include start from rip/prefix-list.xml.i --> <node name="prefix-list"> <properties> <help>Prefix-list</help> diff --git a/interface-definitions/include/rip/rip-prefix-list6.xml.i b/interface-definitions/include/rip/prefix-list6.xml.i index fcf1499e0..84b6846fe 100644 --- a/interface-definitions/include/rip/rip-prefix-list6.xml.i +++ b/interface-definitions/include/rip/prefix-list6.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-prefix-list.xml.i --> +<!-- include start from rip/prefix-list.xml.i --> <node name="prefix-list"> <properties> <help>Prefix-list</help> diff --git a/interface-definitions/include/rip/rip-redistribute.xml.i b/interface-definitions/include/rip/redistribute.xml.i index d7a79b007..34154a526 100644 --- a/interface-definitions/include/rip/rip-redistribute.xml.i +++ b/interface-definitions/include/rip/redistribute.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-redistribute.xml.i --> +<!-- include start from rip/redistribute.xml.i --> <leafNode name="metric"> <properties> <help>Metric for redistributed routes</help> diff --git a/interface-definitions/include/rip/rip-timers.xml.i b/interface-definitions/include/rip/timers.xml.i index 129d9ed23..771a6700e 100644 --- a/interface-definitions/include/rip/rip-timers.xml.i +++ b/interface-definitions/include/rip/timers.xml.i @@ -1,4 +1,4 @@ -<!-- include start from rip/rip-timers.xml.i --> +<!-- include start from rip/timers.xml.i --> <node name="timers"> <properties> <help>RIPng timer values</help> diff --git a/interface-definitions/include/rip/version.xml.i b/interface-definitions/include/rip/version.xml.i new file mode 100644 index 000000000..a35350aee --- /dev/null +++ b/interface-definitions/include/rip/version.xml.i @@ -0,0 +1,18 @@ +<!-- include start from rip/version.xml.i -->
+<leafNode name="version">
+ <properties>
+ <help>Limit RIP protocol version</help>
+ <valueHelp>
+ <format>1</format>
+ <description>Allow RIPv1 only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>2</format>
+ <description>Allow RIPv2 only</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/version/monitoring-version.xml.i b/interface-definitions/include/version/monitoring-version.xml.i new file mode 100644 index 000000000..6a275a5d8 --- /dev/null +++ b/interface-definitions/include/version/monitoring-version.xml.i @@ -0,0 +1,3 @@ +<!-- include start from include/version/monitoring-version.xml.i --> +<syntaxVersion component='monitoring' 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 6d0c80518..426173a19 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='2'></syntaxVersion> +<syntaxVersion component='policy' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i index 3cf92001c..b7650c782 100644 --- a/interface-definitions/include/version/system-version.xml.i +++ b/interface-definitions/include/version/system-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/system-version.xml.i --> -<syntaxVersion component='system' version='24'></syntaxVersion> +<syntaxVersion component='system' version='25'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 96dede723..8b6c6ef62 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -94,6 +94,23 @@ #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> + <leafNode name="mii-mon-interval"> + <properties> + <help>Specifies the MII link monitoring frequency in milliseconds</help> + <valueHelp> + <format>u32:0</format> + <description>Disable MII link monitoring</description> + </valueHelp> + <valueHelp> + <format>u32:50-1000</format> + <description>MII link monitoring frequency in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 50-1000"/> + </constraint> + </properties> + <defaultValue>100</defaultValue> + </leafNode> <leafNode name="min-links"> <properties> <help>Minimum number of member interfaces required up before enabling bond</help> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 60edf3ce2..48ee1efbc 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -73,12 +73,18 @@ </leafNode> <node name="igmp"> <properties> - <help>Internet Group Management Protocol (IGMP) settings</help> + <help>Internet Group Management Protocol (IGMP) and Multicast Listener Discovery (MLD) settings</help> </properties> <children> <leafNode name="querier"> <properties> - <help>Enable IGMP querier</help> + <help>Enable IGMP/MLD querier</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="snooping"> + <properties> + <help>Enable IGMP/MLD snooping</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index edcf7b37f..6cbd91ff4 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -167,6 +167,7 @@ </leafNode> </children> </node> + #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="hash"> @@ -304,10 +305,7 @@ </leafNode> <leafNode name="openvpn-option"> <properties> - <help>Additional OpenVPN options. You must - use the syntax of openvpn.conf in this text-field. Using this - without proper knowledge may result in a crashed OpenVPN server. - Check system log to look for errors.</help> + <help>Additional OpenVPN options. You must use the syntax of openvpn.conf in this text-field. Using this without proper knowledge may result in a crashed OpenVPN server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> @@ -501,10 +499,7 @@ </leafNode> <leafNode name="subnet-mask"> <properties> - <help>Subnet mask pushed to dynamic clients. - If not set the server subnet mask will be used. - Only used with topology subnet or device type tap. - Not used with bridged interfaces.</help> + <help>Subnet mask pushed to dynamic clients. If not set the server subnet mask will be used. Only used with topology subnet or device type tap. Not used with bridged interfaces.</help> <constraint> <validator name="ipv4-address"/> </constraint> @@ -746,7 +741,7 @@ </properties> </leafNode> #include <include/pki/certificate.xml.i> - #include <include/pki/ca-certificate.xml.i> + #include <include/pki/ca-certificate-multi.xml.i> <leafNode name="dh-params"> <properties> <help>Diffie Hellman parameters (server only)</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 664914baa..9674cfc0e 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> <properties> - <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> + <help>Point-to-Point Protocol over Ethernet (PPPoE) Interface</help> <priority>322</priority> <constraint> <regex>pppoe[0-9]+</regex> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 6b62f4c61..53e6445fa 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py"> <properties> - <help>Pseudo Ethernet</help> + <help>Pseudo Ethernet Interface (Macvlan)</help> <priority>321</priority> <constraint> <regex>peth[0-9]+</regex> diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in index b471c3b92..aa83a04b2 100644 --- a/interface-definitions/interfaces-vti.xml.in +++ b/interface-definitions/interfaces-vti.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="vti" owner="${vyos_conf_scripts_dir}/interfaces-vti.py"> <properties> - <help>Virtual Tunnel interface</help> + <help>Virtual Tunnel Interface (XFRM)</help> <priority>381</priority> <constraint> <regex>vti[0-9]+</regex> @@ -16,19 +16,7 @@ </valueHelp> </properties> <children> - <leafNode name="address"> - <properties> - <help>IP address</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-host"/> - </constraint> - <multi/> - </properties> - </leafNode> + #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index eb6107303..daee770a9 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -716,9 +716,7 @@ </leafNode> <leafNode name="passphrase"> <properties> - <help>WPA personal shared pass phrase. If you are - using special characters in the WPA passphrase then single - quotes are required.</help> + <help>WPA personal shared pass phrase. If you are using special characters in the WPA passphrase then single quotes are required.</help> <valueHelp> <format>txt</format> <description>Passphrase of at least 8 but not more than 63 printable characters</description> diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in index a518a9def..85636a50f 100644 --- a/interface-definitions/ntp.xml.in +++ b/interface-definitions/ntp.xml.in @@ -81,6 +81,7 @@ </leafNode> </children> </node> + #include <include/generic-interface-multi.xml.i> #include <include/listen-address.xml.i> #include <include/interface/vrf.xml.i> </children> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 6d137c2ce..c4fde2c78 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -3,6 +3,7 @@ <node name="pki" owner="${vyos_conf_scripts_dir}/pki.py"> <properties> <help>VyOS PKI configuration</help> + <priority>300</priority> </properties> <children> <tagNode name="ca"> diff --git a/interface-definitions/policy-local-route.xml.in b/interface-definitions/policy-local-route.xml.in index 573a7963f..d969613b1 100644 --- a/interface-definitions/policy-local-route.xml.in +++ b/interface-definitions/policy-local-route.xml.in @@ -146,11 +146,11 @@ <properties> <help>Source address or prefix</help> <valueHelp> - <format>ipv4</format> + <format>ipv6</format> <description>Address to match against</description> </valueHelp> <valueHelp> - <format>ipv4net</format> + <format>ipv6net</format> <description>Prefix to match against</description> </valueHelp> <constraint> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index 1d5d7dd55..0d0ada591 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -637,6 +637,18 @@ </completionHelp> </properties> </leafNode> + <leafNode name="prefix-len"> + <properties> + <help>IP prefix-length to match</help> + <valueHelp> + <format>u32:0-32</format> + <description>Prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + </properties> + </leafNode> </children> </node> <!-- T3304 but it overwrite node nexthop @@ -655,12 +667,20 @@ <node name="nexthop"> <properties> <help>IP next-hop of route to match</help> - <valueHelp> - <format>ipv4</format> - <description>Next-hop IPv4 router address</description> - </valueHelp> </properties> <children> + <leafNode name="address"> + <properties> + <help>IP address to match</help> + <valueHelp> + <format>ipv4</format> + <description>Nexthop IP address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="access-list"> <properties> <help>IP access-list to match</help> @@ -682,6 +702,18 @@ </valueHelp> </properties> </leafNode> + <leafNode name="prefix-len"> + <properties> + <help>IP prefix-length to match</help> + <valueHelp> + <format>u32:0-32</format> + <description>Prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + </properties> + </leafNode> <leafNode name="prefix-list"> <properties> <help>IP prefix-list to match</help> @@ -690,11 +722,26 @@ </completionHelp> </properties> </leafNode> + <leafNode name="type"> + <properties> + <help>Match type</help> + <completionHelp> + <list>blackhole</list> + </completionHelp> + <valueHelp> + <format>blackhole</format> + <description>Blackhole</description> + </valueHelp> + <constraint> + <regex>(blackhole)</regex> + </constraint> + </properties> + </leafNode> </children> </node> <node name="route-source"> <properties> - <help>test</help> + <help>Match advertising source address of route</help> </properties> <children> <leafNode name="access-list"> @@ -760,8 +807,21 @@ </completionHelp> </properties> </leafNode> + <leafNode name="prefix-len"> + <properties> + <help>IPv6 prefix-length to match</help> + <valueHelp> + <format>u32:0-128</format> + <description>Prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-128"/> + </constraint> + </properties> + </leafNode> </children> </node> + <!-- T3976 but it overwrite node nexthop <leafNode name="nexthop"> <properties> <help>IPv6 next-hop of route to match</help> @@ -775,6 +835,62 @@ </properties> </leafNode> </children> + </node> --> + <node name="nexthop"> + <properties> + <help>IPv6 next-hop of route to match</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address of next-hop</help> + <valueHelp> + <format>ipv6</format> + <description>Nexthop IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="access-list"> + <properties> + <help>IPv6 access-list to match</help> + <valueHelp> + <format>txt</format> + <description>IPV6 access list name</description> + </valueHelp> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="prefix-list"> + <properties> + <help>IPv6 prefix-list to match</help> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Match type</help> + <completionHelp> + <list>blackhole</list> + </completionHelp> + <valueHelp> + <format>blackhole</format> + <description>Blackhole</description> + </valueHelp> + <constraint> + <regex>(blackhole)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> </node> <node name="large-community"> <properties> @@ -845,8 +961,13 @@ <format>ipv4</format> <description>Peer IP address</description> </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Peer IPv6 address</description> + </valueHelp> <constraint> <validator name="ipv4-address"/> + <validator name="ipv6-address"/> </constraint> </properties> </leafNode> @@ -1070,6 +1191,44 @@ </constraint> </properties> </leafNode> + <node name="evpn"> + <properties> + <help>Ethernet Virtual Private Network</help> + </properties> + <children> + <node name="gateway"> + <properties> + <help>Set gateway IP for prefix advertisement route</help> + </properties> + <children> + <leafNode name="ipv4"> + <properties> + <help>Set gateway IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>Gateway IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ipv6"> + <properties> + <help>Set gateway IPv6 address</help> + <valueHelp> + <format>ipv6</format> + <description>Gateway IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> <node name="extcommunity"> <properties> <help>BGP extended community attribute</help> @@ -1257,6 +1416,7 @@ <description>Metric value</description> </valueHelp> <constraint> + <validator name="numeric" argument="--relative --"/> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> diff --git a/interface-definitions/protocols-eigrp.xml.in b/interface-definitions/protocols-eigrp.xml.in new file mode 100644 index 000000000..88a881a1e --- /dev/null +++ b/interface-definitions/protocols-eigrp.xml.in @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- Enhanced Interior Gateway Routing Protocol (EIGRP) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="eigrp" owner="${vyos_conf_scripts_dir}/protocols_eigrp.py"> + <properties> + <help>Enhanced Interior Gateway Routing Protocol (EIGRP)</help> + <priority>820</priority> + </properties> + <children> + #include <include/eigrp/protocol-common-config.xml.i> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in index be8e30c18..43ca659e9 100644 --- a/interface-definitions/protocols-mpls.xml.in +++ b/interface-definitions/protocols-mpls.xml.in @@ -6,7 +6,7 @@ <node name="mpls" owner="${vyos_conf_scripts_dir}/protocols_mpls.py"> <properties> <help>Multiprotocol Label Switching (MPLS)</help> - <priority>299</priority> + <priority>400</priority> </properties> <children> <node name="ldp"> diff --git a/interface-definitions/protocols-nhrp.xml.in b/interface-definitions/protocols-nhrp.xml.in index 7de3704ce..d7663c095 100644 --- a/interface-definitions/protocols-nhrp.xml.in +++ b/interface-definitions/protocols-nhrp.xml.in @@ -4,13 +4,13 @@ <children> <node name="nhrp" owner="${vyos_conf_scripts_dir}/protocols_nhrp.py"> <properties> - <help>NHRP parameters</help> + <help>Next Hop Resolution Protocol (NHRP) parameters</help> <priority>680</priority> </properties> <children> <tagNode name="tunnel"> <properties> - <help>Tunnel for NHRP [REQUIRED]</help> + <help>Tunnel for NHRP</help> <constraint> <regex>tun[0-9]+</regex> </constraint> @@ -27,6 +27,10 @@ <format>txt</format> <description>Pass phrase for cisco authentication</description> </valueHelp> + <constraint> + <regex>[^[:space:]]{1,8}</regex> + </constraint> + <constraintErrorMessage>Password should contain up to eight non-whitespace characters</constraintErrorMessage> </properties> </leafNode> <tagNode name="dynamic-map"> @@ -40,7 +44,7 @@ <children> <leafNode name="nbma-domain-name"> <properties> - <help>Set HUB fqdn (nbma-address - fqdn) [REQUIRED]</help> + <help>Set HUB fqdn (nbma-address - fqdn)</help> <valueHelp> <format><fqdn></format> <description>Set the external HUB fqdn</description> @@ -67,7 +71,7 @@ </leafNode> <leafNode name="nbma-address"> <properties> - <help>Set HUB address (nbma-address - external hub address or fqdn) [REQUIRED]</help> + <help>Set HUB address (nbma-address - external hub address or fqdn)</help> </properties> </leafNode> <leafNode name="register"> diff --git a/interface-definitions/protocols-rip.xml.in b/interface-definitions/protocols-rip.xml.in index bbb88aef1..2195b0316 100644 --- a/interface-definitions/protocols-rip.xml.in +++ b/interface-definitions/protocols-rip.xml.in @@ -20,14 +20,14 @@ </constraint> </properties> </leafNode> - #include <include/rip/rip-default-information.xml.i> - #include <include/rip/rip-default-metric.xml.i> + #include <include/rip/default-information.xml.i> + #include <include/rip/default-metric.xml.i> <node name="distribute-list"> <properties> <help>Filter networks in routing updates</help> </properties> <children> - #include <include/rip/rip-access-list.xml.i> + #include <include/rip/access-list.xml.i> <tagNode name="interface"> <properties> <help>Apply filtering to an interface</help> @@ -43,14 +43,14 @@ </constraint> </properties> <children> - #include <include/rip/rip-access-list.xml.i> - #include <include/rip/rip-prefix-list.xml.i> + #include <include/rip/access-list.xml.i> + #include <include/rip/prefix-list.xml.i> </children> </tagNode> - #include <include/rip/rip-prefix-list.xml.i> + #include <include/rip/prefix-list.xml.i> </children> </node> - #include <include/rip/rip-interface.xml.i> + #include <include/rip/interface.xml.i> <tagNode name="interface"> <children> <node name="authentication"> @@ -98,6 +98,22 @@ <constraintErrorMessage>Password must be 16 characters or less</constraintErrorMessage> </properties> </leafNode> + </children> + </node> + <node name="receive"> + <properties> + <help>Advertisement reception</help> + </properties> + <children> + #include <include/rip/version.xml.i> + </children> + </node> + <node name="send"> + <properties> + <help>Advertisement transmission</help> + </properties> + <children> + #include <include/rip/version.xml.i> </children> </node> </children> @@ -166,7 +182,7 @@ <help>Redistribute BGP routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="connected"> @@ -174,7 +190,7 @@ <help>Redistribute connected routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="isis"> @@ -182,7 +198,7 @@ <help>Redistribute IS-IS routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="kernel"> @@ -190,7 +206,7 @@ <help>Redistribute kernel routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="ospf"> @@ -198,7 +214,7 @@ <help>Redistribute OSPF routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="static"> @@ -206,7 +222,7 @@ <help>Redistribute static routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> </children> @@ -224,10 +240,12 @@ <multi/> </properties> </leafNode> - #include <include/rip/rip-timers.xml.i> + #include <include/rip/timers.xml.i> #include <include/route-map.xml.i> + #include <include/rip/version.xml.i> </children> </node> </children> </node> </interfaceDefinition> + diff --git a/interface-definitions/protocols-ripng.xml.in b/interface-definitions/protocols-ripng.xml.in index fe7411e65..d7e4b2514 100644 --- a/interface-definitions/protocols-ripng.xml.in +++ b/interface-definitions/protocols-ripng.xml.in @@ -21,14 +21,14 @@ <multi/> </properties> </leafNode> - #include <include/rip/rip-default-information.xml.i> - #include <include/rip/rip-default-metric.xml.i> + #include <include/rip/default-information.xml.i> + #include <include/rip/default-metric.xml.i> <node name="distribute-list"> <properties> <help>Filter networks in routing updates</help> </properties> <children> - #include <include/rip/rip-access-list6.xml.i> + #include <include/rip/access-list6.xml.i> <tagNode name="interface"> <properties> <help>Apply filtering to an interface</help> @@ -44,14 +44,14 @@ </constraint> </properties> <children> - #include <include/rip/rip-access-list6.xml.i> - #include <include/rip/rip-prefix-list6.xml.i> + #include <include/rip/access-list6.xml.i> + #include <include/rip/prefix-list6.xml.i> </children> </tagNode> - #include <include/rip/rip-prefix-list6.xml.i> + #include <include/rip/prefix-list6.xml.i> </children> </node> - #include <include/rip/rip-interface.xml.i> + #include <include/rip/interface.xml.i> <leafNode name="network"> <properties> <help>RIPng network</help> @@ -88,7 +88,7 @@ <help>Redistribute BGP routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="connected"> @@ -96,7 +96,7 @@ <help>Redistribute connected routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="kernel"> @@ -104,7 +104,7 @@ <help>Redistribute kernel routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="ospfv3"> @@ -112,7 +112,7 @@ <help>Redistribute OSPFv3 routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> <node name="static"> @@ -120,7 +120,7 @@ <help>Redistribute static routes</help> </properties> <children> - #include <include/rip/rip-redistribute.xml.i> + #include <include/rip/redistribute.xml.i> </children> </node> </children> @@ -139,7 +139,7 @@ </properties> </leafNode> #include <include/route-map.xml.i> - #include <include/rip/rip-timers.xml.i> + #include <include/rip/timers.xml.i> </children> </node> </children> diff --git a/interface-definitions/protocols-static.xml.in b/interface-definitions/protocols-static.xml.in index 3cc28e296..e89433022 100644 --- a/interface-definitions/protocols-static.xml.in +++ b/interface-definitions/protocols-static.xml.in @@ -7,7 +7,7 @@ <children> <node name="static" owner="${vyos_conf_scripts_dir}/protocols_static.py"> <properties> - <help>Static route parameters</help> + <help>Static Routing</help> <priority>480</priority> </properties> <children> diff --git a/interface-definitions/service_conntrack-sync.xml.in b/interface-definitions/service-conntrack-sync.xml.in index 32efa7323..6fa6fc5f9 100644 --- a/interface-definitions/service_conntrack-sync.xml.in +++ b/interface-definitions/service-conntrack-sync.xml.in @@ -5,7 +5,8 @@ <node name="conntrack-sync" owner="${vyos_conf_scripts_dir}/conntrack_sync.py"> <properties> <help>Connection tracking synchronization</help> - <priority>995</priority> + <!-- before VRRP / HA --> + <priority>799</priority> </properties> <children> <leafNode name="accept-protocol"> diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service-console-server.xml.in index e9591ad87..e9591ad87 100644 --- a/interface-definitions/service_console-server.xml.in +++ b/interface-definitions/service-console-server.xml.in diff --git a/interface-definitions/service-event-handler.xml.in b/interface-definitions/service-event-handler.xml.in new file mode 100644 index 000000000..aef6bc1bc --- /dev/null +++ b/interface-definitions/service-event-handler.xml.in @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="event-handler" owner="${vyos_conf_scripts_dir}/service_event_handler.py"> + <properties> + <help>Service event handler</help> + </properties> + <children> + <tagNode name="event"> + <properties> + <help>Event handler name</help> + </properties> + <children> + <node name="filter"> + <properties> + <help>Logs filter settings</help> + </properties> + <children> + <leafNode name="pattern"> + <properties> + <help>Match pattern (regex)</help> + </properties> + </leafNode> + <leafNode name="syslog-identifier"> + <properties> + <help>Identifier of a process in syslog (string)</help> + </properties> + </leafNode> + </children> + </node> + <node name="script"> + <properties> + <help>Event handler script file</help> + </properties> + <children> + <leafNode name="arguments"> + <properties> + <help>Script arguments</help> + </properties> + </leafNode> + <tagNode name="environment"> + <properties> + <help>Script environment arguments</help> + </properties> + <children> + <leafNode name="value"> + <properties> + <help>Environment value</help> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="path"> + <properties> + <help>Path to the script</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index e222467b1..cd3aa3638 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -213,6 +213,11 @@ </tagNode> </children> </tagNode> + <node name="radius"> + <children> + #include <include/accel-ppp/radius-additions-rate-limit.xml.i> + </children> + </node> #include <include/radius-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> </children> diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service-mdns-repeater.xml.in index 9a94f1488..9a94f1488 100644 --- a/interface-definitions/service_mdns-repeater.xml.in +++ b/interface-definitions/service-mdns-repeater.xml.in diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service-monitoring-telegraf.xml.in index ff4c8c55f..36f40a539 100644 --- a/interface-definitions/service_monitoring_telegraf.xml.in +++ b/interface-definitions/service-monitoring-telegraf.xml.in @@ -13,41 +13,141 @@ <help>Telegraf monitoring</help> </properties> <children> - <node name="authentication"> + <node name="influxdb"> <properties> - <help>Authentication parameters</help> + <help>Output plugin InfluxDB</help> </properties> <children> - <leafNode name="organization"> + <node name="authentication"> + <properties> + <help>Authentication parameters</help> + </properties> + <children> + <leafNode name="organization"> + <properties> + <help>Authentication organization for InfluxDB v2</help> + <constraint> + <regex>[a-zA-Z][1-9a-zA-Z@_\-.]{2,50}</regex> + </constraint> + <constraintErrorMessage>Organization name must be alphanumeric and can contain hyphens, underscores and at symbol.</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="token"> + <properties> + <help>Authentication token for InfluxDB v2</help> + <valueHelp> + <format>txt</format> + <description>Authentication token</description> + </valueHelp> + <constraint> + <regex>[a-zA-Z0-9-_]{86}==</regex> + </constraint> + <constraintErrorMessage>Token must be 88 characters long and must contain only [a-zA-Z0-9-_] and '==' characters.</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="bucket"> + <properties> + <help>Remote bucket</help> + </properties> + <defaultValue>main</defaultValue> + </leafNode> + #include <include/monitoring/url.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>8086</defaultValue> + </leafNode> + </children> + </node> + <node name="azure-data-explorer"> + <properties> + <help>Output plugin Azure Data Explorer</help> + </properties> + <children> + <node name="authentication"> <properties> - <help>Authentication organization for InfluxDB v2 [REQUIRED]</help> + <help>Authentication parameters</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>Application client id</help> + <constraint> + <regex>[-_a-zA-Z0-9]+</regex> + </constraint> + <constraintErrorMessage>Client-id is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="client-secret"> + <properties> + <help>Application client secret</help> + <constraint> + <regex>[-_a-zA-Z0-9]+</regex> + </constraint> + <constraintErrorMessage>Client-secret is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tenant-id"> + <properties> + <help>Set tenant id</help> + <constraint> + <regex>[-_a-zA-Z0-9]+</regex> + </constraint> + <constraintErrorMessage>Tenant-id is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="database"> + <properties> + <help>Remote database name</help> + <valueHelp> + <format>txt</format> + <description>Remote database name</description> + </valueHelp> <constraint> - <regex>[a-zA-Z][1-9a-zA-Z@_\-.]{2,50}</regex> + <regex>[-_a-zA-Z0-9]+</regex> </constraint> - <constraintErrorMessage>Organization name must be alphanumeric and can contain hyphens, underscores and at symbol.</constraintErrorMessage> + <constraintErrorMessage>Database is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> </properties> </leafNode> - <leafNode name="token"> + <leafNode name="group-metrics"> <properties> - <help>Authentication token for InfluxDB v2 [REQUIRED]</help> + <help>Type of metrics grouping when push to Azure Data Explorer</help> + <completionHelp> + <list>single-table table-per-metric</list> + </completionHelp> + <valueHelp> + <format>single-table</format> + <description>Metrics stores in one table</description> + </valueHelp> + <valueHelp> + <format>table-per-metric</format> + <description>One table per gorups of metric by the metric name</description> + </valueHelp> + <constraint> + <regex>(single-table|table-per-metric)</regex> + </constraint> + </properties> + <defaultValue>table-per-metric</defaultValue> + </leafNode> + <leafNode name="table"> + <properties> + <help>Name of the single table [Only if set group-metrics single-table]</help> <valueHelp> <format>txt</format> - <description>Authentication token</description> + <description>Table name</description> </valueHelp> <constraint> - <regex>[a-zA-Z0-9-_]{86}==</regex> + <regex>[-_a-zA-Z0-9]+</regex> </constraint> - <constraintErrorMessage>Token must be 88 characters long and must contain only [a-zA-Z0-9-_] and '==' characters.</constraintErrorMessage> + <constraintErrorMessage>Table is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> </properties> </leafNode> + #include <include/monitoring/url.xml.i> </children> </node> - <leafNode name="bucket"> - <properties> - <help>Remote bucket</help> - </properties> - <defaultValue>main</defaultValue> - </leafNode> <leafNode name="source"> <properties> <help>Source parameters for monitoring</help> @@ -193,7 +293,7 @@ </node> <leafNode name="url"> <properties> - <help>Remote URL [REQUIRED]</help> + <help>Remote URL</help> <valueHelp> <format>url</format> <description>Remote URL to Splunk collector</description> @@ -206,23 +306,6 @@ </leafNode> </children> </node> - <leafNode name="url"> - <properties> - <help>Remote URL [REQUIRED]</help> - <valueHelp> - <format>url</format> - <description>Remote URL to InfluxDB v2</description> - </valueHelp> - <constraint> - <regex>(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?</regex> - </constraint> - <constraintErrorMessage>Incorrect URL format.</constraintErrorMessage> - </properties> - </leafNode> - #include <include/port-number.xml.i> - <leafNode name="port"> - <defaultValue>8086</defaultValue> - </leafNode> </children> </node> </children> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service-pppoe-server.xml.in index 50f42849b..50f42849b 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service-pppoe-server.xml.in diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service-router-advert.xml.in index bb11e9cd0..258b7b749 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service-router-advert.xml.in @@ -10,7 +10,7 @@ <children> <tagNode name="interface"> <properties> - <help>Interface to send RA on [REQUIRED]</help> + <help>Interface to send RA on</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> @@ -136,6 +136,23 @@ </children> </node> #include <include/name-server-ipv6.xml.i> + <leafNode name="name-server-lifetime"> + <properties> + <help>Maximum duration how long the RDNSS entries are used</help> + <valueHelp> + <format>u32:0</format> + <description>Name-servers should no longer be used</description> + </valueHelp> + <valueHelp> + <format>u32:1-7200</format> + <description>Maximum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-7200"/> + </constraint> + <constraintErrorMessage>Maximum interval must be between 1 and 7200 seconds</constraintErrorMessage> + </properties> + </leafNode> <leafNode name="other-config-flag"> <properties> <help>Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information</help> diff --git a/interface-definitions/service-sla.xml.in b/interface-definitions/service-sla.xml.in new file mode 100644 index 000000000..0c4f8a591 --- /dev/null +++ b/interface-definitions/service-sla.xml.in @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="sla" owner="${vyos_conf_scripts_dir}/service_sla.py"> + <properties> + <help>Service level agreement (SLA)</help> + </properties> + <children> + <node name="owamp-server"> + <properties> + <help>One-way active measurement protocol (OWAMP) server</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>861</defaultValue> + </leafNode> + </children> + </node> + <node name="twamp-server"> + <properties> + <help>Two-way active measurement protocol (TWAMP) server</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>862</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service-upnp.xml.in index a129b7260..a129b7260 100644 --- a/interface-definitions/service_upnp.xml.in +++ b/interface-definitions/service-upnp.xml.in diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service-webproxy.xml.in index 9a75bc27d..e4609b699 100644 --- a/interface-definitions/service_webproxy.xml.in +++ b/interface-definitions/service-webproxy.xml.in @@ -288,7 +288,7 @@ </leafNode> <tagNode name="listen-address"> <properties> - <help>IPv4 listen-address for WebProxy [REQUIRED]</help> + <help>IPv4 listen-address for WebProxy</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> </completionHelp> @@ -452,7 +452,7 @@ </leafNode> <leafNode name="source-group"> <properties> - <help>Source-group for this rule [REQUIRED]</help> + <help>Source-group for this rule</help> <valueHelp> <format>group</format> <description>Source group identifier for this rule</description> @@ -484,7 +484,7 @@ <description>Name of source group</description> </valueHelp> <constraint> - <regex>[^0-9]</regex> + <regex>[^0-9][a-zA-Z_][a-zA-Z0-9][\w\-\.]*</regex> </constraint> <constraintErrorMessage>URL-filter source-group cannot start with a number!</constraintErrorMessage> </properties> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index 8edbad110..126183162 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -61,6 +61,78 @@ <valueless/> </properties> </leafNode> + <node name="dynamic-protection"> + <properties> + <help>Allow dynamic protection</help> + </properties> + <children> + <leafNode name="block-time"> + <properties> + <help>Block source IP in seconds. Subsequent blocks increase by a factor of 1.5</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Time interval in seconds for blocking</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>120</defaultValue> + </leafNode> + <leafNode name="detect-time"> + <properties> + <help>Remember source IP in seconds before reset their score</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Time interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>1800</defaultValue> + </leafNode> + <leafNode name="threshold"> + <properties> + <help>Block source IP when their cumulative attack score exceeds threshold</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Threshold score</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="allow-from"> + <properties> + <help>Always allow inbound connections from these systems</help> + <valueHelp> + <format>ipv4</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> <leafNode name="key-exchange"> <properties> <help>Allowed key exchange (KEX) algorithms</help> diff --git a/interface-definitions/intel_qat.xml.in b/interface-definitions/system-acceleration-qat.xml.in index 812484184..812484184 100644 --- a/interface-definitions/intel_qat.xml.in +++ b/interface-definitions/system-acceleration-qat.xml.in diff --git a/interface-definitions/system-frr.xml.in b/interface-definitions/system-frr.xml.in new file mode 100644 index 000000000..9fe23ed75 --- /dev/null +++ b/interface-definitions/system-frr.xml.in @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="frr" owner="${vyos_conf_scripts_dir}/system_frr.py"> + <properties> + <help>Configure FRR parameters</help> + <!-- Before components that use FRR --> + <priority>150</priority> + </properties> + <children> + <leafNode name="bmp"> + <properties> + <help>Enable BGP Monitoring Protocol support</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="irdp"> + <properties> + <help>Enable ICMP Router Discovery Protocol support</help> + <valueless/> + </properties> + </leafNode> + <node name="snmp"> + <properties> + <help>Enable SNMP integration for next daemons</help> + </properties> + <children> + <leafNode name="bgpd"> + <properties> + <help>BGP</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="isisd"> + <properties> + <help>IS-IS</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ldpd"> + <properties> + <help>LDP</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ospf6d"> + <properties> + <help>OSPFv3</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ospfd"> + <properties> + <help>OSPFv2</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ripd"> + <properties> + <help>RIP</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="zebra"> + <properties> + <help>Zebra (IP routing manager)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in index 21d70694b..e00dbf252 100644 --- a/interface-definitions/system-ip.xml.in +++ b/interface-definitions/system-ip.xml.in @@ -23,6 +23,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="disable-directed-broadcast"> + <properties> + <help>Disable IPv4 directed broadcast forwarding on all interfaces</help> + <valueless/> + </properties> + </leafNode> <node name="multipath"> <properties> <help>IPv4 multipath settings</help> diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in index 9b1a15317..0cf4de308 100644 --- a/interface-definitions/system-lcd.xml.in +++ b/interface-definitions/system-lcd.xml.in @@ -10,7 +10,7 @@ <children> <leafNode name="model"> <properties> - <help>Model of the display attached to this system [REQUIRED]</help> + <help>Model of the display attached to this system</help> <completionHelp> <list>cfa-533 cfa-631 cfa-633 cfa-635 hd44780 sdec</list> </completionHelp> diff --git a/interface-definitions/system-syslog.xml.in b/interface-definitions/system-syslog.xml.in index 480cb1ca6..90c3de5c1 100644 --- a/interface-definitions/system-syslog.xml.in +++ b/interface-definitions/system-syslog.xml.in @@ -390,31 +390,6 @@ <help>Logging to system standard location</help> </properties> <children> - <node name="archive"> - <properties> - <help>Log file size and rotation characteristics</help> - </properties> - <children> - <leafNode name="file"> - <properties> - <help>Number of saved files (default is 5)</help> - <constraint> - <regex>[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="size"> - <properties> - <help>Size of log files (in kbytes, default is 256)</help> - <constraint> - <regex>[0-9]+</regex> - </constraint> - <constraintErrorMessage>illegal characters in size</constraintErrorMessage> - </properties> - </leafNode> - </children> - </node> <tagNode name="facility"> <properties> <help>Facility for logging</help> diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in index 4963eab3c..8ca4da883 100644 --- a/interface-definitions/tftp-server.xml.in +++ b/interface-definitions/tftp-server.xml.in @@ -11,7 +11,7 @@ <children> <leafNode name="directory"> <properties> - <help>Folder containing files served by TFTP [REQUIRED]</help> + <help>Folder containing files served by TFTP</help> </properties> </leafNode> <leafNode name="allow-upload"> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn-ipsec.xml.in index 555ba689f..d36fbb024 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn-ipsec.xml.in @@ -19,7 +19,7 @@ </leafNode> <tagNode name="esp-group"> <properties> - <help>Encapsulated Security Payload (ESP) group name</help> + <help>Encapsulating Security Payload (ESP) group name</help> </properties> <children> <leafNode name="compression"> @@ -44,10 +44,10 @@ </leafNode> <leafNode name="lifetime"> <properties> - <help>ESP lifetime</help> + <help>Security Association time to expire</help> <valueHelp> <format>u32:30-86400</format> - <description>ESP lifetime in seconds</description> + <description>SA lifetime in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 30-86400"/> @@ -57,10 +57,10 @@ </leafNode> <leafNode name="life-bytes"> <properties> - <help>ESP life in bytes</help> + <help>Security Association byte count to expire</help> <valueHelp> <format>u32:1024-26843545600000</format> - <description>ESP life in bytes</description> + <description>SA life in bytes</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1024-26843545600000"/> @@ -69,10 +69,10 @@ </leafNode> <leafNode name="life-packets"> <properties> - <help>ESP life in packets</help> + <help>Security Association packet count to expire</help> <valueHelp> <format>u32:1000-26843545600000</format> - <description>ESP life in packets</description> + <description>SA life in packets</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1000-26843545600000"/> @@ -209,7 +209,7 @@ </leafNode> <tagNode name="proposal"> <properties> - <help>ESP group proposal [REQUIRED]</help> + <help>ESP group proposal</help> <valueHelp> <format>u32:1-65535</format> <description>ESP group proposal number</description> @@ -308,13 +308,13 @@ </node> <leafNode name="ikev2-reauth"> <properties> - <help>Re-authentication of the remote peer during an IKE re-key - IKEv2 only</help> + <help>Re-authentication of the remote peer during an IKE re-key (IKEv2 only)</help> <completionHelp> <list>yes no</list> </completionHelp> <valueHelp> <format>yes</format> - <description>Enable remote host re-authentication during an IKE rekey. Currently broken due to a strongswan bug</description> + <description>Enable remote host re-authentication during an IKE rekey (currently broken due to a strongswan bug)</description> </valueHelp> <valueHelp> <format>no</format> @@ -379,7 +379,7 @@ </leafNode> <leafNode name="mode"> <properties> - <help>IKEv1 phase 1 mode selection</help> + <help>IKEv1 phase 1 mode</help> <completionHelp> <list>main aggressive</list> </completionHelp> @@ -530,10 +530,10 @@ <children> <leafNode name="level"> <properties> - <help>strongSwan logging Level</help> + <help>Global IPsec logging Level</help> <valueHelp> <format>0</format> - <description>Very basic auditing logs e.g. SA up/SA down</description> + <description>Very basic auditing logs (e.g., SA up/SA down)</description> </valueHelp> <valueHelp> <format>1</format> @@ -663,13 +663,13 @@ </node> <tagNode name="profile"> <properties> - <help>VPN IPSec profile</help> + <help>VPN IPsec profile</help> </properties> <children> #include <include/generic-disable-node.xml.i> <node name="authentication"> <properties> - <help>Authentication [REQUIRED]</help> + <help>Authentication</help> </properties> <children> <leafNode name="mode"> @@ -689,7 +689,7 @@ </node> <node name="bind"> <properties> - <help>DMVPN crypto configuration</help> + <help>DMVPN tunnel configuration</help> </properties> <children> <leafNode name="tunnel"> @@ -951,7 +951,7 @@ #include <include/generic-disable-node.xml.i> <node name="authentication"> <properties> - <help>Peer authentication [REQUIRED]</help> + <help>Peer authentication</help> </properties> <children> #include <include/ipsec/authentication-id.xml.i> @@ -1010,7 +1010,7 @@ </valueHelp> <valueHelp> <format>respond</format> - <description>Bring the connection up only if traffic is detected</description> + <description>Wait for the peer to initiate the connection</description> </valueHelp> <valueHelp> <format>none</format> @@ -1077,10 +1077,10 @@ #include <include/ipsec/local-address.xml.i> <tagNode name="tunnel"> <properties> - <help>Peer tunnel [REQUIRED]</help> + <help>Peer tunnel</help> <valueHelp> <format>u32</format> - <description>Peer tunnel [REQUIRED]</description> + <description>Peer tunnel</description> </valueHelp> </properties> <children> @@ -1090,10 +1090,10 @@ #include <include/ip-protocol.xml.i> <leafNode name="priority"> <properties> - <help>Priority for IPSec policy (lowest value more preferable)</help> + <help>Priority for IPsec policy (lowest value more preferable)</help> <valueHelp> <format>u32:1-100</format> - <description>Priority for IPSec policy (lowest value more preferable)</description> + <description>Priority for IPsec policy (lowest value more preferable)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-100"/> @@ -1144,7 +1144,7 @@ </leafNode> <node name="vti"> <properties> - <help>Virtual tunnel interface [REQUIRED]</help> + <help>Virtual tunnel interface</help> </properties> <children> <leafNode name="bind"> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index f734283e7..f734283e7 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index 7981c3fa2..21b47125d 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in @@ -51,6 +51,82 @@ </children> </node> #include <include/auth-local-users.xml.i> + <node name="local-users"> + <children> + <tagNode name="username"> + <children> + <node name="otp"> + <properties> + <help>2FA OTP authentication parameters</help> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>Token Key Secret key for the token algorithm (see RFC 4226)</help> + <valueHelp> + <format>txt</format> + <description>OTP key in hex-encoded format</description> + </valueHelp> + <constraint> + <regex>[a-fA-F0-9]{20,10000}</regex> + </constraint> + <constraintErrorMessage>Key name must only include hex characters and be at least 20 characters long</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="otp-length"> + <properties> + <help>Number of digits in OTP code</help> + <valueHelp> + <format>u32:6-8</format> + <description>Number of digits in OTP code</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 6-8"/> + </constraint> + <constraintErrorMessage>Number of digits in OTP code must be between 6 and 8</constraintErrorMessage> + </properties> + <defaultValue>6</defaultValue> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Time tokens interval in seconds</help> + <valueHelp> + <format>u32:5-86400</format> + <description>Time tokens interval in seconds.</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-86400"/> + </constraint> + <constraintErrorMessage>Time token interval must be between 5 and 86400 seconds</constraintErrorMessage> + </properties> + <defaultValue>30</defaultValue> + </leafNode> + <leafNode name="token-type"> + <properties> + <help>Token type</help> + <valueHelp> + <format>hotp-time</format> + <description>Time-based OTP algorithm</description> + </valueHelp> + <valueHelp> + <format>hotp-event</format> + <description>Event-based OTP algorithm</description> + </valueHelp> + <constraint> + <regex>(hotp-time|hotp-event)</regex> + </constraint> + <completionHelp> + <list>hotp-time hotp-event</list> + </completionHelp> + </properties> + <defaultValue>hotp-time</defaultValue> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> #include <include/radius-server-ipv4.xml.i> <node name="radius"> <children> diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn-pptp.xml.in index 28a53acb9..28a53acb9 100644 --- a/interface-definitions/vpn_pptp.xml.in +++ b/interface-definitions/vpn-pptp.xml.in diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index fe2fea9f8..195d581df 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in @@ -37,6 +37,10 @@ </children> </node> #include <include/accel-ppp/client-ipv6-pool.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>443</defaultValue> + </leafNode> <node name="ppp-options"> <properties> <help>PPP (Point-to-Point Protocol) settings</help> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 14c31fa8a..3604b41c8 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -28,6 +28,22 @@ <children> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> + <node name="ip"> + <properties> + <help>IPv4 routing parameters</help> + </properties> + <children> + #include <include/interface/disable-forwarding.xml.i> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 routing parameters</help> + </properties> + <children> + #include <include/interface/disable-forwarding.xml.i> + </children> + </node> <node name="protocols"> <properties> <help>Routing protocol parameters</help> @@ -42,6 +58,15 @@ #include <include/bgp/protocol-common-config.xml.i> </children> </node> + <node name="eigrp" owner="${vyos_conf_scripts_dir}/protocols_eigrp.py $VAR(../../@)"> + <properties> + <help>Enhanced Interior Gateway Routing Protocol (EIGRP)</help> + <priority>821</priority> + </properties> + <children> + #include <include/eigrp/protocol-common-config.xml.i> + </children> + </node> <node name="isis" owner="${vyos_conf_scripts_dir}/protocols_isis.py $VAR(../../@)"> <properties> <help>Intermediate System to Intermediate System (IS-IS)</help> @@ -71,7 +96,7 @@ </node> <node name="static" owner="${vyos_conf_scripts_dir}/protocols_static.py $VAR(../../@)"> <properties> - <help>Static route parameters</help> + <help>Static Routing</help> <priority>481</priority> </properties> <children> diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in index 8af0dcfb6..dca4c59d1 100644 --- a/interface-definitions/zone-policy.xml.in +++ b/interface-definitions/zone-policy.xml.in @@ -19,6 +19,7 @@ </properties> <children> #include <include/generic-description.xml.i> + #include <include/firewall/name-default-log.xml.i> <leafNode name="default-action"> <properties> <help>Default-action for traffic coming into this zone</help> diff --git a/op-mode-definitions/clear-dhcp-server-lease.xml.in b/op-mode-definitions/clear-dhcp-server-lease.xml.in new file mode 100644 index 000000000..b1241588c --- /dev/null +++ b/op-mode-definitions/clear-dhcp-server-lease.xml.in @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="clear"> + <children> + <node name="dhcp-server"> + <properties> + <help>clear DHCP server lease</help> + </properties> + <children> + <tagNode name="lease"> + <properties> + <help>DHCP server lease</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/clear_dhcp_lease.py --ip $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/containers.xml.in b/op-mode-definitions/container.xml.in index 48501bd84..fa66402dc 100644 --- a/op-mode-definitions/containers.xml.in +++ b/op-mode-definitions/container.xml.in @@ -11,7 +11,7 @@ <properties> <help>Pull a new image for container</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --pull "${4}"</command> + <command>sudo podman image pull "${4}"</command> </tagNode> </children> </node> @@ -44,7 +44,7 @@ <script>sudo podman image ls -q</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --remove "${4}"</command> + <command>sudo podman image rm --force "${4}"</command> </tagNode> </children> </node> @@ -100,13 +100,13 @@ <properties> <help>Show containers</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --all</command> + <command>sudo podman ps --all</command> <children> <leafNode name="image"> <properties> <help>Show container image</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --image</command> + <command>sudo podman image ls</command> </leafNode> <tagNode name="log"> <properties> @@ -121,7 +121,7 @@ <properties> <help>Show available container networks</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --networks</command> + <command>sudo podman network ls</command> </leafNode> </children> </node> @@ -167,7 +167,7 @@ <path>container name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/containers_op.py --update "${4}"</command> + <command>if cli-shell-api existsActive container name "$4"; then sudo podman pull $(cli-shell-api returnActiveValue container name "$4" image); else echo "Container $4 does not exist"; fi</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index 6574f2319..baf60efbd 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -1,5 +1,46 @@ <?xml version="1.0"?> <interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <children> + <node name="dns"> + <properties> + <help>Monitor last lines of Domain Name Service (DNS)</help> + </properties> + <children> + <node name="forwarding"> + <properties> + <help>Monitor last lines of DNS forwarding</help> + </properties> + <command>journalctl --no-hostname --follow --boot --unit pdns-recursor.service</command> + </node> + </children> + </node> + </children> + </node> + <node name="dns"> + <properties> + <help>Show DNS information</help> + </properties> + <children> + <node name="forwarding"> + <properties> + <help>Show DNS forwarding information</help> + </properties> + <children> + <leafNode name="statistics"> + <properties> + <help>Show DNS forwarding statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> <node name="show"> <children> <node name="log"> @@ -13,7 +54,7 @@ <properties> <help>Show log for DNS Forwarding</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "pdns_recursor"</command> + <command>journalctl --no-hostname --boot --unit pdns-recursor.service</command> </node> </children> </node> diff --git a/op-mode-definitions/force-wamp.xml.in b/op-mode-definitions/force-wamp.xml.in new file mode 100644 index 000000000..dbb205c6b --- /dev/null +++ b/op-mode-definitions/force-wamp.xml.in @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <tagNode name="owping"> + <properties> + <help>IP address of the remote OWAMP server</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>owping $3</command> + </tagNode> + <tagNode name="twping"> + <properties> + <help>IP address of the remote TWAMP server</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>twping $3</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/geoip.xml.in b/op-mode-definitions/geoip.xml.in new file mode 100644 index 000000000..c1b6e87b9 --- /dev/null +++ b/op-mode-definitions/geoip.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="update"> + <children> + <leafNode name="geoip"> + <properties> + <help>Update GeoIP database and firewall sets</help> + </properties> + <command>sudo ${vyos_libexec_dir}/geoip-update.py --force</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/include/bgp/reset-bgp-afi-common.xml.i b/op-mode-definitions/include/bgp/reset-bgp-afi-common.xml.i new file mode 100644 index 000000000..2f88daad3 --- /dev/null +++ b/op-mode-definitions/include/bgp/reset-bgp-afi-common.xml.i @@ -0,0 +1,20 @@ +<!-- included start from bgp/reset-bgp-afi-common.xml.i --> +<node name="external"> + <properties> + <help>Reset all external peers</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> +</node> +<tagNode name="1-4294967295"> + <properties> + <help>Reset peers with the AS number</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/bgp/reset-bgp-neighbor-options.xml.i b/op-mode-definitions/include/bgp/reset-bgp-neighbor-options.xml.i new file mode 100644 index 000000000..d9feee18a --- /dev/null +++ b/op-mode-definitions/include/bgp/reset-bgp-neighbor-options.xml.i @@ -0,0 +1,48 @@ +<!-- included start from bgp/reset-bgp-neighbor-options.xml.i --> +<node name="in"> + <properties> + <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <leafNode name="prefix-filter"> + <properties> + <help>Push out prefix-list ORF and do inbound soft reconfig</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> +</node> +<leafNode name="message-stats"> + <properties> + <help>Reset message statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<leafNode name="out"> + <properties> + <help>Resend all outbound updates</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<node name="soft"> + <properties> + <help>Soft reconfig inbound and outbound updates</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="in"> + <properties> + <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + <node name="out"> + <properties> + <help>Resend all outbound updates</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> +</node> +<!-- included end --> diff --git a/op-mode-definitions/include/bgp/reset-bgp-peer-group-vrf.xml.i b/op-mode-definitions/include/bgp/reset-bgp-peer-group-vrf.xml.i new file mode 100644 index 000000000..c1a24bae2 --- /dev/null +++ b/op-mode-definitions/include/bgp/reset-bgp-peer-group-vrf.xml.i @@ -0,0 +1,14 @@ +<!-- included start from bgp/reset-bgp-peer-group-vrf.xml.i --> +<tagNode name="peer-group"> + <properties> + <help>Reset all members of peer-group</help> + <completionHelp> + <path>vrf name ${COMP_WORDS[4]} protocols bgp peer-group</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/include/bgp/reset-bgp-peer-group.xml.i b/op-mode-definitions/include/bgp/reset-bgp-peer-group.xml.i new file mode 100644 index 000000000..c26e47b47 --- /dev/null +++ b/op-mode-definitions/include/bgp/reset-bgp-peer-group.xml.i @@ -0,0 +1,14 @@ +<!-- included start from bgp/reset-bgp-peer-group.xml.i --> +<tagNode name="peer-group"> + <properties> + <help>Reset all members of peer-group</help> + <completionHelp> + <path>protocols bgp peer-group</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> +</tagNode> +<!-- included end --> diff --git a/op-mode-definitions/ipv6-route.xml.in b/op-mode-definitions/ipv6-route.xml.in index 5f20444d4..4f8792f9f 100644 --- a/op-mode-definitions/ipv6-route.xml.in +++ b/op-mode-definitions/ipv6-route.xml.in @@ -7,7 +7,7 @@ <children> <node name="ipv6"> <properties> - <help>Show IPv6 routing information</help> + <help>Show IPv6 networking information</help> </properties> <children> <leafNode name="groups"> @@ -16,14 +16,32 @@ </properties> <command>netstat -gn6</command> </leafNode> - - <leafNode name="neighbors"> + <node name="neighbors"> <properties> - <help>Show IPv6 Neighbor Discovery (ND) information</help> + <help>Show IPv6 neighbor (NDP) table</help> </properties> <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6</command> - </leafNode> - + <children> + <tagNode name="interface"> + <properties> + <help>Show IPv6 neighbor table for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --interface "$5"</command> + </tagNode> + <tagNode name="state"> + <properties> + <help>Show IPv6 neighbors with specified state</help> + <completionHelp> + <list>reachable stale failed permanent</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet6 --state "$5"</command> + </tagNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 6f82ce611..f5e0ede59 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -8,7 +8,7 @@ </properties> <command>journalctl --no-hostname --follow --boot</command> <children> - <node name="colored"> + <node name="color"> <properties> <help>Output log in a colored fashion</help> </properties> @@ -16,18 +16,18 @@ </node> <node name="dhcp"> <properties> - <help>Show log for Dynamic Host Control Protocol (DHCP)</help> + <help>Monitor last lines of Dynamic Host Control Protocol (DHCP)</help> </properties> <children> <node name="server"> <properties> - <help>Show log for DHCP server</help> + <help>Monitor last lines of DHCP server</help> </properties> <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server.service</command> </node> <node name="client"> <properties> - <help>Show DHCP client logs</help> + <help>Monitor last lines of DHCP client</help> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhclient@*.service"</command> <children> @@ -46,18 +46,18 @@ </node> <node name="dhcpv6"> <properties> - <help>Show log for Dynamic Host Control Protocol IPv6 (DHCPv6)</help> + <help>Monitor last lines of Dynamic Host Control Protocol IPv6 (DHCPv6)</help> </properties> <children> <node name="server"> <properties> - <help>Show log for DHCPv6 server</help> + <help>Monitor last lines of DHCPv6 server</help> </properties> <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server6.service</command> </node> <node name="client"> <properties> - <help>Show DHCPv6 client logs</help> + <help>Monitor last lines of DHCPv6 client</help> </properties> <command>journalctl --no-hostname --follow --boot --unit "dhcp6c@*.service"</command> <children> @@ -74,12 +74,24 @@ </node> </children> </node> + <leafNode name="flow-accounting"> + <properties> + <help>Monitor last lines of flow-accounting log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit uacctd.service</command> + </leafNode> <leafNode name="kernel"> <properties> <help>Monitor last lines of Linux Kernel log</help> </properties> <command>journalctl --no-hostname --boot --follow --dmesg</command> </leafNode> + <leafNode name="nhrp"> + <properties> + <help>Monitor last lines of NHRP log</help> + </properties> + <command>journalctl --no-hostname --boot --unit opennhrp.service</command> + </leafNode> <node name="pppoe"> <properties> <help>Monitor last lines of PPPoE log</help> diff --git a/op-mode-definitions/openconnect.xml.in b/op-mode-definitions/openconnect.xml.in index 6b0082b4c..9343637c0 100644 --- a/op-mode-definitions/openconnect.xml.in +++ b/op-mode-definitions/openconnect.xml.in @@ -13,6 +13,53 @@ </properties> <command>${vyos_op_scripts_dir}/openconnect-control.py --action="show_sessions"</command> </leafNode> + <tagNode name="user"> + <properties> + <help>Show OpenConnect configured user settings</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_openconnect_users.py</script> + </completionHelp> + </properties> + <children> + <node name="otp"> + <properties> + <help>Show OTP key information</help> + </properties> + <children> + <leafNode name="full"> + <properties> + <help>Show full settings, including QR code and commands for VyOS</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openconnect_otp.py --user="$4" --info="full"</command> + </leafNode> + <leafNode name="key-hex"> + <properties> + <help>Show OTP authentication secret in Hex (used in VyOS config)</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openconnect_otp.py --user="$4" --info="key-hex"</command> + </leafNode> + <leafNode name="key-b32"> + <properties> + <help>Show OTP authentication secret in Base32 (used in mobile apps)</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openconnect_otp.py --user="$4" --info="key-b32"</command> + </leafNode> + <leafNode name="qrcode"> + <properties> + <help>Show OTP authentication QR code</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openconnect_otp.py --user="$4" --info="qrcode"</command> + </leafNode> + <leafNode name="uri"> + <properties> + <help>Show OTP authentication otpauth URI</help> + </properties> + <command>${vyos_op_scripts_dir}/show_openconnect_otp.py --user="$4" --info="uri"</command> + </leafNode> + </children> + </node> + </children> + </tagNode> </children> </node> </children> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index a1c55dcf4..346febec0 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -349,6 +349,141 @@ </node> </children> </node> + <node name="import"> + <properties> + <help>Import an object</help> + </properties> + <children> + <node name="pki"> + <properties> + <help>Import file into PKI configuration</help> + </properties> + <children> + <tagNode name="ca"> + <properties> + <help>Import CA certificate into PKI</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Path to CA certificate file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --filename "$6"</command> + </tagNode> + <tagNode name="key-file"> + <properties> + <help>Path to private key file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --ca "$4" --key-filename "$6"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="certificate"> + <properties> + <help>Import certificate into PKI</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Path to certificate file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --filename "$6"</command> + </tagNode> + <tagNode name="key-file"> + <properties> + <help>Path to private key file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --certificate "$4" --key-filename "$6"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="crl"> + <properties> + <help>Import certificate revocation list into PKI</help> + <completionHelp> + <list><CA name></list> + </completionHelp> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Path to CRL file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --crl "$4" --filename "$6"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="dh"> + <properties> + <help>Import DH parameters into PKI</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Path to DH parameters file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --dh "$4" --filename "$6"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="key-pair"> + <properties> + <help>Import key pair into PKI</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <children> + <tagNode name="public-file"> + <properties> + <help>Path to public key file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --filename "$6"</command> + </tagNode> + <tagNode name="private-file"> + <properties> + <help>Path to private key file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --keypair "$4" --key-filename "$6"</command> + </tagNode> + </children> + </tagNode> + <node name="openvpn"> + <properties> + <help>Import OpenVPN keys into PKI</help> + </properties> + <children> + <tagNode name="shared-secret"> + <properties> + <help>Import OpenVPN shared secret key into PKI</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <children> + <tagNode name="file"> + <properties> + <help>Path to shared secret key file</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action import --openvpn "$5" --filename "$7"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> <node name="show"> <children> <node name="pki"> diff --git a/op-mode-definitions/reset-bgp.xml.in b/op-mode-definitions/reset-bgp.xml.in new file mode 100644 index 000000000..a1d42d4a3 --- /dev/null +++ b/op-mode-definitions/reset-bgp.xml.in @@ -0,0 +1,258 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="bgp"> + <properties> + <help>Border Gateway Protocol (BGP) information</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group.xml.i> + <tagNode name="prefix"> + <properties> + <help>Clear bestpath and re-advertise</help> + <completionHelp> + <list><x.x.x.x/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </tagNode> + <node name="ipv4"> + <properties> + <help>IPv4 Address Family</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp ipv4 *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group.xml.i> + </children> + </node> + <tagNode name="ipv4"> + <properties> + <help>IPv4 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv4</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + <node name="ipv6"> + <properties> + <help>IPv6 Address Family</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp ipv6 *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group.xml.i> + </children> + </node> + <tagNode name="ipv6"> + <properties> + <help>IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv6</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + <node name="l2vpn"> + <properties> + <help>Layer 2 Virtual Private Network Address Family</help> + </properties> + <children> + <node name="evpn"> + <properties> + <help>Ethernet Virtual Private Network</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp l2vpn evpn *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group.xml.i> + </children> + </node> + <tagNode name="evpn"> + <properties> + <help>BGP IPv4/IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --both</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + </children> + </node> + <tagNode name="vrf"> + <properties> + <help>Virtual Routing and Forwarding (VRF)</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <node name="node.tag"> + <properties> + <help>IPv4/IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --both --vrf ${COMP_WORDS[3]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </node> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp vrf $4 *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group-vrf.xml.i> + <tagNode name="prefix"> + <properties> + <help>Clear bestpath and re-advertise</help> + <completionHelp> + <list><x.x.x.x/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </tagNode> + <node name="ipv4"> + <properties> + <help>IPv4 Address Family</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp vrf $4 ipv4 *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group-vrf.xml.i> + </children> + </node> + <tagNode name="ipv4"> + <properties> + <help>IPv4 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv4 --vrf ${COMP_WORDS[3]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + <node name="ipv6"> + <properties> + <help>IPv6 Address Family</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp vrf $4 ipv6 *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group-vrf.xml.i> + </children> + </node> + <tagNode name="ipv6"> + <properties> + <help>IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv6 --vrf ${COMP_WORDS[3]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + <node name="l2vpn"> + <properties> + <help>Layer 2 Virtual Private Network Address Family</help> + </properties> + <children> + <node name="evpn"> + <properties> + <help>Ethernet Virtual Private Network</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Clear all peers</help> + </properties> + <command>vtysh -c "clear bgp vrf $4 l2vpn evpn *"</command> + </leafNode> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group-vrf.xml.i> + </children> + </node> + <tagNode name="evpn"> + <properties> + <help>BGP IPv4/IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --both --vrf ${COMP_WORDS[3]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <tagNode name="bgp"> + <properties> + <help>BGP IPv4/IPv6 neighbor to clear</help> + <completionHelp> + <script>${vyos_completion_dir}/list_bgp_neighbors.sh --both</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-ip-bgp.xml.in b/op-mode-definitions/reset-ip-bgp.xml.in index 931a2a9bc..34a4503d9 100644 --- a/op-mode-definitions/reset-ip-bgp.xml.in +++ b/op-mode-definitions/reset-ip-bgp.xml.in @@ -6,7 +6,7 @@ <children> <node name="bgp"> <properties> - <help>Clear Border Gateway Protocol (BGP) statistics or status</help> + <help>Border Gateway Protocol (BGP) information</help> </properties> <children> <leafNode name="all"> @@ -41,159 +41,45 @@ </leafNode> </children> </tagNode> - <node name="external"> + #include <include/bgp/reset-bgp-afi-common.xml.i> + #include <include/bgp/reset-bgp-peer-group.xml.i> + <tagNode name="vrf"> <properties> - <help>Clear all external peers</help> + <help>Clear BGP statistics or status for vrf</help> <completionHelp> - <list>WORD</list> + <path>vrf name</path> </completionHelp> </properties> - <command>vtysh -c "clear bgp ipv4 external"</command> <children> - <node name="in"> + <leafNode name="all"> <properties> - <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> + <help>Clear all BGP peering sessions for vrf</help> </properties> - <command>vtysh -c "clear bgp ipv4 external in"</command> - <children> - <leafNode name="prefix-filter"> - <properties> - <help>Push out prefix-list ORF and do inbound soft reconfig</help> - </properties> - <command>vtysh -c "clear bgp ipv4 external in prefix-filter"</command> - </leafNode> - </children> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 external out"</command> - </node> - <node name="soft"> - <properties> - <help>Soft reconfig inbound and outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 external soft"</command> - <children> - <node name="in"> - <properties> - <help>Clear via soft reconfig of inbound update</help> - </properties> - <command>vtysh -c "clear bgp ipv4 external soft in"</command> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 external soft out"</command> - </node> - </children> - </node> - </children> - </node> - <tagNode name="peer-group"> - <properties> - <help>Clear BGP statistics or status for given peer-group</help> - <completionHelp> - <list>WORD</list> - </completionHelp> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5"</command> - <children> - <node name="in"> - <properties> - <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 in"</command> - <children> - <leafNode name="prefix-filter"> - <properties> - <help>Push out prefix-list ORF and do inbound soft reconfig</help> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 in prefix-filter"</command> - </leafNode> - </children> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 out"</command> - </node> - <node name="soft"> + <command>vtysh -c "clear bgp vrf $5 *"</command> + </leafNode> + <leafNode name="node.tag"> <properties> - <help>Soft reconfig inbound and outbound updates</help> + <help>Clear BGP neighbor IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 soft"</command> - <children> - <node name="in"> - <properties> - <help>Clear via soft reconfig of inbound update</help> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 soft in"</command> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 peer-group $5 soft out"</command> - </node> - </children> - </node> + <command>vtysh -c "clear bgp vrf $5 $6"</command> + </leafNode> </children> </tagNode> </children> </node> <tagNode name="bgp"> <properties> - <help>Clear BGP neighbor IP address</help> + <help>BGP IPv4/IPv6 neighbor to clear</help> <completionHelp> <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv4</script> </completionHelp> </properties> - <command>vtysh -c "clear bgp ipv4 $4"</command> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> - <node name="in"> - <properties> - <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 in"</command> - <children> - <leafNode name="prefix-filter"> - <properties> - <help>Push out prefix-list ORF and do inbound soft reconfig</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 in prefix-filter"</command> - </leafNode> - </children> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 out"</command> - </node> - <node name="soft"> - <properties> - <help>Soft reconfig inbound and outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 soft"</command> - <children> - <node name="in"> - <properties> - <help>Clear via soft reconfig of inbound update</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 soft in"</command> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv4 $4 soft out"</command> - </node> - </children> - </node> + #include <include/bgp/reset-bgp-neighbor-options.xml.i> </children> </tagNode> </children> diff --git a/op-mode-definitions/reset-ipv6-bgp.xml.in b/op-mode-definitions/reset-ipv6-bgp.xml.in deleted file mode 100644 index 3c4275331..000000000 --- a/op-mode-definitions/reset-ipv6-bgp.xml.in +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> - <node name="reset"> - <children> - <node name="ipv6"> - <children> - <tagNode name="bgp"> - <properties> - <help>Clear BGP neighbor IP address</help> - <completionHelp> - <script>${vyos_completion_dir}/list_bgp_neighbors.sh --ipv6</script> - </completionHelp> - </properties> - <command>vtysh -c "clear bgp ipv6 $4"</command> - <children> - <node name="in"> - <properties> - <help>Send route-refresh unless using 'soft-reconfiguration inbound'</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 in"</command> - <children> - <leafNode name="prefix-filter"> - <properties> - <help>Push out prefix-list ORF and do inbound soft reconfig</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 in prefix-filter"</command> - </leafNode> - </children> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 out"</command> - </node> - <node name="soft"> - <properties> - <help>Soft reconfig inbound and outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 soft"</command> - <children> - <node name="in"> - <properties> - <help>Clear via soft reconfig of inbound update</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 soft in"</command> - </node> - <node name="out"> - <properties> - <help>Resend all outbound updates</help> - </properties> - <command>vtysh -c "clear bgp ipv6 $4 soft out"</command> - </node> - </children> - </node> - </children> - </tagNode> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/op-mode-definitions/show-arp.xml.in b/op-mode-definitions/show-arp.xml.in index 12e7d3aa2..58cc6e45e 100644 --- a/op-mode-definitions/show-arp.xml.in +++ b/op-mode-definitions/show-arp.xml.in @@ -6,7 +6,7 @@ <properties> <help>Show Address Resolution Protocol (ARP) information</help> </properties> - <command>/usr/sbin/arp -e -n</command> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command> <children> <tagNode name="interface"> <properties> @@ -15,7 +15,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -b</script> </completionHelp> </properties> - <command>/usr/sbin/arp -e -n -i "$4"</command> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$4"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-conntrack.xml.in b/op-mode-definitions/show-conntrack.xml.in new file mode 100644 index 000000000..792623d7d --- /dev/null +++ b/op-mode-definitions/show-conntrack.xml.in @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="conntrack"> + <properties> + <help>Show conntrack tables entries</help> + </properties> + <children> + <node name="table"> + <properties> + <help>Show conntrack entries for table</help> + </properties> + <children> + <node name="ipv4"> + <properties> + <help>Show conntrack entries for IPv4 protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_conntrack.py</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in index 91564440d..d342ac192 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -4,14 +4,34 @@ <children> <node name="ip"> <properties> - <help>Show IPv4 routing information</help> + <help>Show IPv4 networking information</help> </properties> <children> <node name="neighbors"> <properties> - <help>Show IPv4 Neighbor Discovery (ND) information</help> + <help>Show IPv4 neighbor (ARP) table</help> </properties> <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show IPv4 neighbor table for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$5"</command> + </tagNode> + <tagNode name="state"> + <properties> + <help>Show IPv4 neighbors with specified state</help> + <completionHelp> + <list>reachable stale failed permanent</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --state "$5"</command> + </tagNode> + </children> </node> </children> </node> diff --git a/op-mode-definitions/show-ipv6.xml.in b/op-mode-definitions/show-ipv6.xml.in index a59c8df0c..66bc2485a 100644 --- a/op-mode-definitions/show-ipv6.xml.in +++ b/op-mode-definitions/show-ipv6.xml.in @@ -4,7 +4,7 @@ <children> <node name="ipv6"> <properties> - <help>Show IPv6 routing information</help> + <help>Show IPv6 networking information</help> </properties> <children> <node name="access-list"> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 954369712..76879e5d6 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -139,6 +139,12 @@ </tagNode> </children> </node> + <leafNode name="flow-accounting"> + <properties> + <help>Show log for flow-accounting</help> + </properties> + <command>journalctl --no-hostname --boot --unit uacctd.service</command> + </leafNode> <leafNode name="https"> <properties> <help>Show log for HTTPs</help> @@ -195,6 +201,12 @@ </properties> <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command> </leafNode> + <leafNode name="nhrp"> + <properties> + <help>Show log for NHRP</help> + </properties> + <command>journalctl --no-hostname --boot --unit opennhrp.service</command> + </leafNode> <node name="openvpn"> <properties> <help>Show log for OpenVPN</help> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 0f852164e..68b473bc1 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -166,9 +166,9 @@ </leafNode> <leafNode name="uptime"> <properties> - <help>Show how long the system has been up</help> + <help>Show system uptime and load averages</help> </properties> - <command>uptime</command> + <command>${vyos_op_scripts_dir}/show_uptime.py</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/webproxy.xml.in b/op-mode-definitions/webproxy.xml.in index f8ec8fb0a..5ae1577d8 100644 --- a/op-mode-definitions/webproxy.xml.in +++ b/op-mode-definitions/webproxy.xml.in @@ -12,7 +12,7 @@ <properties> <help>Monitor the last lines of the squid access log</help> </properties> - <command>if [ -f /var/log/squid3/access.log ]; then sudo tail --follow=name /var/log/squid3/access.log; else echo "WebProxy cache-log does not exist"; fi</command> + <command>if [ -f /var/log/squid/access.log ]; then sudo tail --follow=name /var/log/squid/access.log; else echo "WebProxy access-log does not exist"; fi</command> </node> <node name="background"> <properties> @@ -37,7 +37,7 @@ <properties> <help>Monitor the last lines of the squid cache log</help> </properties> - <command>if [ -f /var/log/squid3/cache.log ]; then sudo tail --follow=name /var/log/squid3/cache.log; else echo "WebProxy cache-log does not exist"; fi</command> + <command>if [ -f /var/log/squid/cache.log ]; then sudo tail --follow=name /var/log/squid/cache.log; else echo "WebProxy cache-log does not exist"; fi</command> </node> </children> </node> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 04ddc10e9..29d89520c 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -201,11 +201,12 @@ def is_member(conf, interface, intftype=None): intftype is optional, if not passed it will search all known types (currently bridge and bonding) - Returns: - None -> Interface is not a member - interface name -> Interface is a member of this interface - False -> interface type cannot have members + Returns: dict + empty -> Interface is not a member + key -> Interface is a member of this interface """ + from vyos.ifconfig import Section + ret_val = {} intftypes = ['bonding', 'bridge'] @@ -221,9 +222,18 @@ def is_member(conf, interface, intftype=None): for intf in conf.list_nodes(base): member = base + [intf, 'member', 'interface', interface] if conf.exists(member): - tmp = conf.get_config_dict(member, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - ret_val.update({intf : tmp}) + member_type = Section.section(interface) + # Check if it's a VLAN (QinQ) interface + interface = interface.split('.') + if len(interface) == 3: + if conf.exists(['interfaces', member_type, interface[0], 'vif-s', interface[1], 'vif-c', interface[2]]): + ret_val.update({intf : {}}) + elif len(interface) == 2: + if conf.exists(['interfaces', member_type, interface[0], 'vif', interface[1]]): + ret_val.update({intf : {}}) + else: + if conf.exists(['interfaces', member_type, interface[0]]): + ret_val.update({intf : {}}) return ret_val @@ -358,13 +368,14 @@ def get_pppoe_interfaces(conf, vrf=None): """ Common helper functions to retrieve all interfaces from current CLI sessions that have DHCP configured. """ pppoe_interfaces = {} + conf.set_level([]) for ifname in conf.list_nodes(['interfaces', 'pppoe']): # always reset config level, as get_interface_dict() will alter it conf.set_level([]) # we already have a dict representation of the config from get_config_dict(), # but with the extended information from get_interface_dict() we also # get the DHCP client default-route-distance default option if not specified. - ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname) + _, ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname) options = {} if 'default_route_distance' in ifconfig: @@ -455,8 +466,8 @@ def get_interface_dict(config, base, ifname=''): if bond: dict.update({'is_bond_member' : bond}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['dhcp-options'], recursive=True) - if dhcp: dict.update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'dhcp-options']) + if dhcp: dict.update({'dhcp_options_changed' : {}}) # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the @@ -515,8 +526,8 @@ def get_interface_dict(config, base, ifname=''): if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif', vif, 'dhcp-options'], recursive=True) - if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options']) + if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}}) for vif_s, vif_s_config in dict.get('vif_s', {}).items(): # Add subinterface name to dictionary @@ -554,8 +565,8 @@ def get_interface_dict(config, base, ifname=''): if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif-s', vif_s, 'dhcp-options'], recursive=True) - if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options']) + if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}}) for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): # Add subinterface name to dictionary @@ -594,8 +605,8 @@ def get_interface_dict(config, base, ifname=''): {'is_bridge_member' : bridge}) # Check if any DHCP options changed which require a client restat - dhcp = node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'], recursive=True) - if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : ''}) + dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options']) + if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}}) # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, base + [ifname], dict) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index f28ad09c5..3a60f6d92 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -33,6 +33,7 @@ INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url'] REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] +RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset'] # Default "commit via" string APP = "vyos-http-api" @@ -200,3 +201,6 @@ class ConfigSession(object): out = self.__run_command(SHOW + path) return out + def reset(self, path): + out = self.__run_command(RESET + path) + return out diff --git a/python/vyos/cpu.py b/python/vyos/cpu.py new file mode 100644 index 000000000..a0ef864be --- /dev/null +++ b/python/vyos/cpu.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# Copyright 2022 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/>. + +""" +Retrieves (or at least attempts to retrieve) the total number of real CPU cores +installed in a Linux system. + +The issue of core count is complicated by existence of SMT, e.g. Intel's Hyper Threading. +GNU nproc returns the number of LOGICAL cores, +which is 2x of the real cores if SMT is enabled. + +The idea is to find all physical CPUs and add up their core counts. +It has special cases for x86_64 and MAY work correctly on other architectures, +but nothing is certain. +""" + +import re + + +def _read_cpuinfo(): + with open('/proc/cpuinfo', 'r') as f: + return f.readlines() + +def _split_line(l): + l = l.strip() + parts = re.split(r'\s*:\s*', l) + return (parts[0], ":".join(parts[1:])) + +def _find_cpus(cpuinfo_lines): + # Make a dict because it's more convenient to work with later, + # when we need to find physicall distinct CPUs there. + cpus = {} + + cpu_number = 0 + + for l in cpuinfo_lines: + key, value = _split_line(l) + if key == 'processor': + cpu_number = value + cpus[cpu_number] = {} + else: + cpus[cpu_number][key] = value + + return cpus + +def _find_physical_cpus(): + cpus = _find_cpus(_read_cpuinfo()) + + phys_cpus = {} + + for num in cpus: + if 'physical id' in cpus[num]: + # On at least some architectures, CPUs in different sockets + # have different 'physical id' field, e.g. on x86_64. + phys_id = cpus[num]['physical id'] + if phys_id not in phys_cpus: + phys_cpus[phys_id] = cpus[num] + else: + # On other architectures, e.g. on ARM, there's no such field. + # We just assume they are different CPUs, + # whether single core ones or cores of physical CPUs. + phys_cpus[num] = cpu[num] + + return phys_cpus + +def get_cpus(): + """ Returns a list of /proc/cpuinfo entries that belong to different CPUs. + """ + cpus_dict = _find_physical_cpus() + return list(cpus_dict.values()) + +def get_core_count(): + """ Returns the total number of physical CPU cores + (even if Hyper-Threading or another SMT is enabled and has inflated + the number of cores in /proc/cpuinfo) + """ + physical_cpus = _find_physical_cpus() + + core_count = 0 + + for num in physical_cpus: + # Some architectures, e.g. x86_64, include a field for core count. + # Since we found unique physical CPU entries, we can sum their core counts. + if 'cpu cores' in physical_cpus[num]: + core_count += int(physical_cpus[num]['cpu cores']) + else: + core_count += 1 + + return core_count diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index ff8623592..3e2de4c3f 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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,10 +14,82 @@ # 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 csv +import gzip +import os import re +from pathlib import Path +from time import strftime + +from vyos.remote import download +from vyos.template import is_ipv4 +from vyos.template import render +from vyos.util import call from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive +from vyos.util import run + + +# Functions for firewall group domain-groups +def get_ips_domains_dict(list_domains): + """ + Get list of IPv4 addresses by list of domains + Ex: get_ips_domains_dict(['ex1.com', 'ex2.com']) + {'ex1.com': ['192.0.2.1'], 'ex2.com': ['192.0.2.2', '192.0.2.3']} + """ + from socket import gethostbyname_ex + from socket import gaierror + + ip_dict = {} + for domain in list_domains: + try: + _, _, ips = gethostbyname_ex(domain) + ip_dict[domain] = ips + except gaierror: + pass + + return ip_dict + +def nft_init_set(group_name, table="filter", family="ip"): + """ + table ip filter { + set GROUP_NAME + type ipv4_addr + flags interval + } + """ + return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}') + + +def nft_add_set_elements(group_name, elements, table="filter", family="ip"): + """ + table ip filter { + set GROUP_NAME { + type ipv4_addr + flags interval + elements = { 192.0.2.1, 192.0.2.2 } + } + """ + elements = ", ".join(elements) + return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ') + +def nft_flush_set(group_name, table="filter", family="ip"): + """ + Flush elements of nft set + """ + return call(f'nft flush set {family} {table} {group_name}') + +def nft_update_set_elements(group_name, elements, table="filter", family="ip"): + """ + Update elements of nft set + """ + flush_set = nft_flush_set(group_name, table="filter", family="ip") + nft_add_set = nft_add_set_elements(group_name, elements, table="filter", family="ip") + return flush_set, nft_add_set + +# END firewall group domain-group (sets) def find_nftables_rule(table, chain, rule_matches=[]): # Find rule in table/chain that matches all criteria and return the handle @@ -49,6 +121,15 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if states: output.append(f'ct state {{{states}}}') + if 'connection_status' in rule_conf and rule_conf['connection_status']: + status = rule_conf['connection_status'] + if status['nat'] == 'destination': + nat_status = '{dnat}' + output.append(f'ct status {nat_status}') + if status['nat'] == 'source': + nat_status = '{snat}' + output.append(f'ct status {nat_status}') + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': proto = rule_conf['protocol'] operator = '' @@ -69,6 +150,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if suffix[0] == '!': suffix = f'!= {suffix[1:]}' output.append(f'{ip_name} {prefix}addr {suffix}') + + if dict_search_args(side_conf, 'geoip', 'country_code'): + operator = '' + if dict_search_args(side_conf, 'geoip', 'inverse_match') != None: + operator = '!=' + output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC_{fw_name}_{rule_id}') if 'mac_address' in side_conf: suffix = side_conf["mac_address"] @@ -108,21 +195,29 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_name} {prefix}addr {operator} $A{def_suffix}_{group_name}') + output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}') + # Generate firewall group domain-group + elif 'domain_group' in group: + group_name = group['domain_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}') elif 'network_group' in group: group_name = group['network_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_name} {prefix}addr {operator} $N{def_suffix}_{group_name}') + output.append(f'{ip_name} {prefix}addr {operator} @N{def_suffix}_{group_name}') if 'mac_group' in group: group_name = group['mac_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'ether {prefix}addr {operator} $M_{group_name}') + output.append(f'ether {prefix}addr {operator} @M_{group_name}') if 'port_group' in group: proto = rule_conf['protocol'] group_name = group['port_group'] @@ -135,11 +230,16 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): operator = '!=' group_name = group_name[1:] - output.append(f'{proto} {prefix}port {operator} $P_{group_name}') + output.append(f'{proto} {prefix}port {operator} @P_{group_name}') if 'log' in rule_conf and rule_conf['log'] == 'enable': 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 "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"') + + if 'log_level' in rule_conf: + log_level = rule_conf['log_level'] + output.append(f'level {log_level}') + if 'hop_limit' in rule_conf: operators = {'eq': '==', 'gt': '>', 'lt': '<'} @@ -148,6 +248,13 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): value = rule_conf['hop_limit'][op] output.append(f'ip6 hoplimit {operator} {value}') + if 'ttl' in rule_conf: + operators = {'eq': '==', 'gt': '>', 'lt': '<'} + for op, operator in operators.items(): + if op in rule_conf['ttl']: + value = rule_conf['ttl'][op] + output.append(f'ip ttl {operator} {value}') + for icmp in ['icmp', 'icmpv6']: if icmp in rule_conf: if 'type_name' in rule_conf[icmp]: @@ -248,3 +355,118 @@ def parse_policy_set(set_conf, def_suffix): mss = set_conf['tcp_mss'] out.append(f'tcp option maxseg size set {mss}') return " ".join(out) + +# GeoIP + +nftables_geoip_conf = '/run/nftables-geoip.conf' +geoip_database = '/usr/share/vyos-geoip/dbip-country-lite.csv.gz' +geoip_lock_file = '/run/vyos-geoip.lock' + +def geoip_load_data(codes=[]): + data = None + + if not os.path.exists(geoip_database): + return [] + + try: + with gzip.open(geoip_database, mode='rt') as csv_fh: + reader = csv.reader(csv_fh) + out = [] + for start, end, code in reader: + if code.lower() in codes: + out.append([start, end, code.lower()]) + return out + except: + print('Error: Failed to open GeoIP database') + return [] + +def geoip_download_data(): + url = 'https://download.db-ip.com/free/dbip-country-lite-{}.csv.gz'.format(strftime("%Y-%m")) + try: + dirname = os.path.dirname(geoip_database) + if not os.path.exists(dirname): + os.mkdir(dirname) + + download(geoip_database, url) + print("Downloaded GeoIP database") + return True + except: + print("Error: Failed to download GeoIP database") + return False + +class GeoIPLock(object): + def __init__(self, file): + self.file = file + + def __enter__(self): + if os.path.exists(self.file): + return False + + Path(self.file).touch() + return True + + def __exit__(self, exc_type, exc_value, tb): + os.unlink(self.file) + +def geoip_update(firewall, force=False): + with GeoIPLock(geoip_lock_file) as lock: + if not lock: + print("Script is already running") + return False + + if not firewall: + print("Firewall is not configured") + return True + + if not os.path.exists(geoip_database): + if not geoip_download_data(): + return False + elif force: + geoip_download_data() + + ipv4_codes = {} + ipv6_codes = {} + + ipv4_sets = {} + ipv6_sets = {} + + # Map country codes to set names + for codes, path in dict_search_recursive(firewall, 'country_code'): + set_name = f'GEOIP_CC_{path[1]}_{path[3]}' + if path[0] == 'name': + for code in codes: + ipv4_codes.setdefault(code, []).append(set_name) + elif path[0] == 'ipv6_name': + for code in codes: + ipv6_codes.setdefault(code, []).append(set_name) + + if not ipv4_codes and not ipv6_codes: + if force: + print("GeoIP not in use by firewall") + return True + + geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes]) + + # Iterate IP blocks to assign to sets + for start, end, code in geoip_data: + ipv4 = is_ipv4(start) + if code in ipv4_codes and ipv4: + ip_range = f'{start}-{end}' if start != end else start + for setname in ipv4_codes[code]: + ipv4_sets.setdefault(setname, []).append(ip_range) + if code in ipv6_codes and not ipv4: + ip_range = f'{start}-{end}' if start != end else start + for setname in ipv6_codes[code]: + ipv6_sets.setdefault(setname, []).append(ip_range) + + render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', { + 'ipv4_sets': ipv4_sets, + 'ipv6_sets': ipv6_sets + }) + + result = run(f'nft -f {nftables_geoip_conf}') + if result != 0: + print('Error: GeoIP failed to update firewall') + return False + + return True diff --git a/python/vyos/frr.py b/python/vyos/frr.py index cbba19ab7..0ffd5cba9 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -85,7 +85,7 @@ LOG.addHandler(ch2) _frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd', - 'bfdd'] + 'bfdd', 'eigrpd'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index 2b9afe109..98bf6162b 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -179,6 +179,21 @@ class BondIf(Interface): """ self.set_interface('bond_lacp_rate', slow_fast) + def set_miimon_interval(self, interval): + """ + Specifies the MII link monitoring frequency in milliseconds. This + determines how often the link state of each slave is inspected for link + failures. A value of zero disables MII link monitoring. A value of 100 + is a good starting point. + + The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_miimon_interval('100') + """ + return self.set_interface('bond_miimon', interval) + def set_arp_interval(self, interval): """ Specifies the ARP link monitoring frequency in milliseconds. @@ -202,16 +217,7 @@ class BondIf(Interface): >>> from vyos.ifconfig import BondIf >>> BondIf('bond0').set_arp_interval('100') """ - if int(interval) == 0: - """ - Specifies the MII link monitoring frequency in milliseconds. - This determines how often the link state of each slave is - inspected for link failures. A value of zero disables MII - link monitoring. A value of 100 is a good starting point. - """ - return self.set_interface('bond_miimon', interval) - else: - return self.set_interface('bond_arp_interval', interval) + return self.set_interface('bond_arp_interval', interval) def get_arp_ip_target(self): """ @@ -381,26 +387,9 @@ class BondIf(Interface): if 'shutdown_required' in config: self.set_admin_state('down') - # ARP monitor targets need to be synchronized between sysfs and CLI. - # Unfortunately an address can't be send twice to sysfs as this will - # result in the following exception: OSError: [Errno 22] Invalid argument. - # - # We remove ALL addresses prior to adding new ones, this will remove - # addresses manually added by the user too - but as we are limited to 16 adresses - # from the kernel side this looks valid to me. We won't run into an error - # when a user added manual adresses which would result in having more - # then 16 adresses in total. - arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) - for addr in arp_tgt_addr: - self.set_arp_ip_target('-' + addr) - - # Add configured ARP target addresses - value = dict_search('arp_monitor.target', config) - if isinstance(value, str): - value = [value] - if value: - for addr in value: - self.set_arp_ip_target('+' + addr) + # Specifies the MII link monitoring frequency in milliseconds + value = config.get('mii_mon_interval') + self.set_miimon_interval(value) # Bonding transmit hash policy value = config.get('hash_policy') @@ -430,6 +419,32 @@ class BondIf(Interface): if mode == '802.3ad': self.set_lacp_rate(config.get('lacp_rate')) + if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']: + tmp = dict_search('arp_monitor.interval', config) + value = tmp if (tmp != None) else '0' + self.set_arp_interval(value) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = dict_search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + # Add (enslave) interfaces to bond value = dict_search('member.interface', config) for interface in (value or []): diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index ffd9c590f..e4db69c1f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -90,6 +90,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', }, + 'multicast_snooping': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_snooping', + }, }} _command_set = {**Interface._command_set, **{ @@ -198,6 +202,18 @@ class BridgeIf(Interface): """ self.set_interface('multicast_querier', enable) + def set_multicast_snooping(self, enable): + """ + Enable or disable multicast snooping on the bridge. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_snooping(1) + """ + self.set_interface('multicast_snooping', enable) + def add_port(self, interface): """ Add physical interface to bridge (member port) @@ -257,6 +273,11 @@ class BridgeIf(Interface): value = '1' if 'stp' in config else '0' self.set_stp(value) + # enable or disable multicast snooping + tmp = dict_search('igmp.snooping', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_snooping(value) + # enable or disable IGMP querier tmp = dict_search('igmp.querier', config) value = '1' if (tmp != None) else '0' diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 22441d1d2..33a7f9a2d 100755..100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -168,6 +168,10 @@ class Interface(Control): 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding', }, + 'ipv4_directed_broadcast': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', + }, 'rp_filter': { 'validate': lambda flt: assert_range(flt,0,3), 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', @@ -234,6 +238,9 @@ class Interface(Control): 'ipv4_forwarding': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding', }, + 'ipv4_directed_broadcast': { + 'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding', + }, 'rp_filter': { 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter', }, @@ -713,6 +720,13 @@ class Interface(Control): return None return self.set_interface('ipv4_forwarding', forwarding) + def set_ipv4_directed_broadcast(self, forwarding): + """ Configure IPv4 directed broadcast forwarding. """ + tmp = self.get_interface('ipv4_directed_broadcast') + if tmp == forwarding: + return None + return self.set_interface('ipv4_directed_broadcast', forwarding) + def set_ipv4_source_validation(self, value): """ Help prevent attacks used by Spoofing IP Addresses. Reverse path @@ -1498,6 +1512,11 @@ class Interface(Control): value = '0' if (tmp != None) else '1' self.set_ipv4_forwarding(value) + # IPv4 directed broadcast forwarding + tmp = dict_search('ip.enable_directed_broadcast', config) + value = '1' if (tmp != None) else '0' + self.set_ipv4_directed_broadcast(value) + # IPv4 source-validation tmp = dict_search('ip.source_validation', config) value = tmp if (tmp != None) else '0' diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index c50cd5ce9..dc99d365a 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 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 @@ -53,3 +53,7 @@ class VTIIf(Interface): self._cmd(cmd.format(**self.config)) self.set_interface('admin_state', 'down') + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py index a2e0daabd..c6e3435ca 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -105,6 +105,11 @@ class Migrator(object): sys_keys = list(sys_versions.keys()) sys_keys.sort() + # XXX 'bgp' needs to follow 'quagga': + if 'bgp' in sys_keys and 'quagga' in sys_keys: + sys_keys.insert(sys_keys.index('quagga'), + sys_keys.pop(sys_keys.index('bgp'))) + rev_versions = {} for key in sys_keys: diff --git a/python/vyos/pki.py b/python/vyos/pki.py index 0b916eaae..cd15e3878 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -247,7 +247,7 @@ def load_private_key(raw_data, passphrase=None, wrap_tags=True): if wrap_tags: raw_data = wrap_private_key(raw_data, passphrase) - if passphrase: + if passphrase is not None: passphrase = bytes(passphrase, 'utf-8') try: @@ -332,6 +332,54 @@ def verify_certificate(cert, ca_cert): except InvalidSignature: return False +def verify_crl(crl, ca_cert): + # Verify CRL was signed by specified CA + if ca_cert.subject != crl.issuer: + return False + + ca_public_key = ca_cert.public_key() + try: + if isinstance(ca_public_key, rsa.RSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + padding=padding.PKCS1v15(), + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, dsa.DSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, ec.EllipticCurvePublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + signature_algorithm=ec.ECDSA(crl.signature_hash_algorithm)) + else: + return False # We cannot verify it + return True + except InvalidSignature: + return False + +def verify_ca_chain(sorted_names, pki_node): + if len(sorted_names) == 1: # Single cert, no chain + return True + + for name in sorted_names: + cert = load_certificate(pki_node[name]['certificate']) + verified = False + for ca_name in sorted_names: + if name == ca_name: + continue + ca_cert = load_certificate(pki_node[ca_name]['certificate']) + if verify_certificate(cert, ca_cert): + verified = True + break + if not verified and name != sorted_names[-1]: + # Only permit top-most certificate to fail verify (e.g. signed by public CA not explicitly in chain) + return False + return True + # Certificate chain def find_parent(cert, ca_certs): @@ -357,3 +405,16 @@ def find_chain(cert, ca_certs): chain.append(parent) return chain + +def sort_ca_chain(ca_names, pki_node): + def ca_cmp(ca_name1, ca_name2, pki_node): + cert1 = load_certificate(pki_node[ca_name1]['certificate']) + cert2 = load_certificate(pki_node[ca_name2]['certificate']) + + if verify_certificate(cert1, cert2): # cert1 is child of cert2 + return -1 + return 1 + + from functools import cmp_to_key + return sorted(ca_names, key=cmp_to_key(lambda cert1, cert2: ca_cmp(cert1, cert2, pki_node))) + diff --git a/python/vyos/template.py b/python/vyos/template.py index 132f5ddde..eb7f06480 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -554,7 +554,7 @@ def nft_default_rule(fw_conf, fw_name): 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 "[{fw_name[:19]}-default-{action_suffix}]"') output.append(nft_action(default_action)) output.append(f'comment "{fw_name} default-action {default_action}"') @@ -564,8 +564,9 @@ def nft_default_rule(fw_conf, fw_name): def nft_state_policy(conf, state, ipv6=False): out = [f'ct state {state}'] - if 'log' in conf and 'enable' in conf['log']: - out.append('log') + if 'log' in conf: + log_level = conf['log'] + out.append(f'log level {log_level}') out.append('counter') @@ -590,6 +591,26 @@ def nft_intra_zone_action(zone_conf, ipv6=False): return f'jump {name_prefix}{name}' return 'return' +@register_filter('nft_nested_group') +def nft_nested_group(out_list, includes, groups, key): + if not vyos_defined(out_list): + out_list = [] + + def add_includes(name): + if key in groups[name]: + for item in groups[name][key]: + if item in out_list: + continue + out_list.append(item) + + if 'include' in groups[name]: + for name_inc in groups[name]['include']: + add_includes(name_inc) + + for name in includes: + add_includes(name) + return out_list + @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ diff --git a/python/vyos/util.py b/python/vyos/util.py index de55e108b..bee5d7aec 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -197,7 +197,7 @@ def read_file(fname, defaultonfailure=None): return defaultonfailure raise e -def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None): +def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False): """ Write content of data to given fname, should defaultonfailure be not None, it is returned on failure to read. @@ -212,7 +212,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N try: """ Write a file to string """ bytes = 0 - with open(fname, 'w') as f: + with open(fname, 'w' if not append else 'a') as f: bytes = f.write(data) chown(fname, user, group) chmod(fname, mode) @@ -757,21 +757,26 @@ def dict_search_args(dict_object, *path): dict_object = dict_object[item] return dict_object -def dict_search_recursive(dict_object, key): +def dict_search_recursive(dict_object, key, path=[]): """ Traverse a dictionary recurisvely and return the value of the key we are looking for. Thankfully copied from https://stackoverflow.com/a/19871956 + + Modified to yield optional path to found keys """ if isinstance(dict_object, list): for i in dict_object: - for x in dict_search_recursive(i, key): - yield x + new_path = path + [i] + for x in dict_search_recursive(i, key, new_path): + yield x elif isinstance(dict_object, dict): if key in dict_object: - yield dict_object[key] - for j in dict_object.values(): - for x in dict_search_recursive(j, key): + new_path = path + [key] + yield dict_object[key], new_path + for k, j in dict_object.items(): + new_path = path + [k] + for x in dict_search_recursive(j, key, new_path): yield x def get_bridge_fdb(interface): diff --git a/scripts/build-command-templates b/scripts/build-command-templates index 876f5877c..c8ae83d9d 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -27,6 +27,7 @@ import copy import functools from lxml import etree as ET +from textwrap import fill # Defaults @@ -130,6 +131,7 @@ def get_properties(p, default=None): # DNS forwarding for instance has multiple defaults - specified as whitespace separated list tmp = ', '.join(default.text.split()) help += f' (default: {tmp})' + help = fill(help, width=64, subsequent_indent='\t\t\t') props["help"] = help except: pass @@ -192,12 +194,12 @@ def get_properties(p, default=None): # so we get to emulate it comp_exprs = [] for i in lists: - comp_exprs.append("echo \"{0}\"".format(i.text)) + comp_exprs.append(f'echo "{i.text}"') for i in paths: - comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + comp_exprs.append(f'/bin/cli-shell-api listNodes {i.text}') for i in scripts: - comp_exprs.append("sh -c \"{0}\"".format(i.text)) - comp_help = " && ".join(comp_exprs) + comp_exprs.append(f'sh -c "{i.text}"') + comp_help = ' && echo " " && '.join(comp_exprs) props["comp_help"] = comp_help except: props["comp_help"] = [] diff --git a/smoketest/bin/vyos-configtest-pki b/smoketest/bin/vyos-configtest-pki new file mode 100755 index 000000000..2f8af0e61 --- /dev/null +++ b/smoketest/bin/vyos-configtest-pki @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022, 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/>. + +from os import system +from vyos.pki import create_private_key +from vyos.pki import create_certificate_request +from vyos.pki import create_certificate +from vyos.pki import create_certificate_revocation_list +from vyos.pki import create_dh_parameters +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key + +subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos'} +ca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos CA'} +subca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos SubCA'} + +ca_cert = '/config/auth/ovpn_test_ca.pem' +ca_key = '/config/auth/ovpn_test_ca.key' +ca_cert_chain = '/config/auth/ovpn_test_chain.pem' +ca_crl = '/config/auth/ovpn_test_ca.crl' +subca_cert = '/config/auth/ovpn_test_subca.pem' +subca_csr = '/tmp/subca.csr' +subca_key = '/config/auth/ovpn_test_subca.key' +ssl_cert = '/config/auth/ovpn_test_server.pem' +ssl_key = '/config/auth/ovpn_test_server.key' +dh_pem = '/config/auth/ovpn_test_dh.pem' +s2s_key = '/config/auth/ovpn_test_site2site.key' +auth_key = '/config/auth/ovpn_test_tls_auth.key' + +def create_cert(subject, cert_path, key_path, sign_by=None, sign_by_key=None, ca=False, sub_ca=False): + priv_key = create_private_key('rsa', 2048) + cert_req = create_certificate_request(subject, priv_key) + cert = create_certificate( + cert_req, + sign_by if sign_by else cert_req, + sign_by_key if sign_by_key else priv_key, + is_ca=ca, is_sub_ca=sub_ca) + + with open(cert_path, 'w') as f: + f.write(encode_certificate(cert)) + + with open(key_path, 'w') as f: + f.write(encode_private_key(priv_key)) + + return cert, priv_key + +def create_empty_crl(crl_path, sign_by, sign_by_key): + crl = create_certificate_revocation_list(sign_by, sign_by_key, [1]) + + with open(crl_path, 'w') as f: + f.write(encode_certificate(crl)) + + return crl + +if __name__ == '__main__': + # Create Root CA + ca_cert_obj, ca_key_obj = create_cert(ca_subject, ca_cert, ca_key, ca=True) + + # Create Empty CRL + create_empty_crl(ca_crl, ca_cert_obj, ca_key_obj) + + # Create Intermediate CA + subca_cert_obj, subca_key_obj = create_cert( + subca_subject, subca_cert, subca_key, + sign_by=ca_cert_obj, sign_by_key=ca_key_obj, + ca=True, sub_ca=True) + + # Create Chain + with open(ca_cert_chain, 'w') as f: + f.write(encode_certificate(subca_cert_obj) + "\n") + f.write(encode_certificate(ca_cert_obj) + "\n") + + # Create Server Cert + create_cert(subject, ssl_cert, ssl_key, sign_by=subca_cert_obj, sign_by_key=subca_key_obj) + + # Create DH params + dh_params = create_dh_parameters() + + with open(dh_pem, 'w') as f: + f.write(encode_dh_parameters(dh_params)) + + # OpenVPN S2S Key + system(f'openvpn --genkey secret {s2s_key}') + + # OpenVPN Auth Key + system(f'openvpn --genkey secret {auth_key}') diff --git a/smoketest/configs/bgp-small-as b/smoketest/configs.no-load/bgp-small-as index 6b953a3f6..6b953a3f6 100644 --- a/smoketest/configs/bgp-small-as +++ b/smoketest/configs.no-load/bgp-small-as diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs.no-load/pki-ipsec index 6fc239d27..6fc239d27 100644 --- a/smoketest/configs/pki-ipsec +++ b/smoketest/configs.no-load/pki-ipsec diff --git a/smoketest/configs/vrf-bgp b/smoketest/configs.no-load/vrf-bgp index 4ad372a36..4ad372a36 100644 --- a/smoketest/configs/vrf-bgp +++ b/smoketest/configs.no-load/vrf-bgp diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index e6f89954f..23186b9b8 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -128,6 +128,10 @@ system { name-server 192.168.0.1 syslog { global { + archive { + file 5 + size 512 + } facility all { level info } diff --git a/smoketest/configs/bgp-dmvpn-spoke b/smoketest/configs/bgp-dmvpn-spoke index 3d7503a9b..39b64b935 100644 --- a/smoketest/configs/bgp-dmvpn-spoke +++ b/smoketest/configs/bgp-dmvpn-spoke @@ -32,7 +32,7 @@ interfaces { nat { source { rule 10 { - log enable + log outbound-interface pppoe1 source { address 172.17.0.0/16 diff --git a/smoketest/configs/bgp-small-internet-exchange b/smoketest/configs/bgp-small-internet-exchange index d51f87c4a..c9da8fa77 100644 --- a/smoketest/configs/bgp-small-internet-exchange +++ b/smoketest/configs/bgp-small-internet-exchange @@ -269,6 +269,14 @@ policy { } } } + rule 31 { + action deny + match { + ipv6 { + nexthop 2001:db8::1 + } + } + } rule 40 { action permit set { diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex index ac5ff5e99..909e6d17b 100644 --- a/smoketest/configs/dialup-router-complex +++ b/smoketest/configs/dialup-router-complex @@ -66,6 +66,27 @@ firewall { action accept protocol icmpv6 } + rule 15 { + action accept + icmpv6 { + type 1 + } + protocol icmpv6 + } + rule 16 { + action accept + icmpv6 { + type 1/1 + } + protocol icmpv6 + } + rule 17 { + action accept + icmpv6 { + type destination-unreachable + } + protocol icmpv6 + } } ipv6-name ALLOW-ESTABLISHED-6 { default-action drop diff --git a/smoketest/configs/dialup-router-medium-vpn b/smoketest/configs/dialup-router-medium-vpn index 63d955738..56722d222 100644 --- a/smoketest/configs/dialup-router-medium-vpn +++ b/smoketest/configs/dialup-router-medium-vpn @@ -120,8 +120,9 @@ interfaces { persistent-tunnel remote-host 192.0.2.10 tls { - ca-cert-file /config/auth/ovpn_test_ca.pem + ca-cert-file /config/auth/ovpn_test_chain.pem cert-file /config/auth/ovpn_test_server.pem + crl-file /config/auth/ovpn_test_ca.crl key-file /config/auth/ovpn_test_server.key auth-file /config/auth/ovpn_test_tls_auth.key } @@ -152,7 +153,7 @@ interfaces { remote-host 01.foo.com remote-port 1194 tls { - ca-cert-file /config/auth/ovpn_test_ca.pem + ca-cert-file /config/auth/ovpn_test_chain.pem auth-file /config/auth/ovpn_test_tls_auth.key } } diff --git a/smoketest/configs/isis-small b/smoketest/configs/isis-small index 247ae32b5..5a4201988 100644 --- a/smoketest/configs/isis-small +++ b/smoketest/configs/isis-small @@ -74,7 +74,6 @@ system { encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ plaintext-password "" } - level admin } } ntp { diff --git a/smoketest/configs/vrf-basic b/smoketest/configs/vrf-basic index ded33f683..20ac7a92f 100644 --- a/smoketest/configs/vrf-basic +++ b/smoketest/configs/vrf-basic @@ -196,7 +196,6 @@ system { } } } - nt ntp { server 0.pool.ntp.org { } diff --git a/smoketest/configs/vrf-ospf b/smoketest/configs/vrf-ospf index 7855e86bf..aae6afb6b 100644 --- a/smoketest/configs/vrf-ospf +++ b/smoketest/configs/vrf-ospf @@ -51,7 +51,6 @@ system { } } } - nt ntp { server 0.pool.ntp.org { } diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 816ba6dcd..55343b893 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -232,6 +232,9 @@ class BasicInterfaceTest: for interface in self._interfaces: base = self._base_path + [interface] + # just set the interface base without any option - some interfaces + # (VTI) do not require any option to be brought up + self.cli_set(base) for option in self._options.get(interface, []): self.cli_set(base + option.split()) @@ -635,6 +638,7 @@ class BasicInterfaceTest: self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo]) self.cli_set(path + ['ip', 'disable-arp-filter']) self.cli_set(path + ['ip', 'disable-forwarding']) + self.cli_set(path + ['ip', 'enable-directed-broadcast']) self.cli_set(path + ['ip', 'enable-arp-accept']) self.cli_set(path + ['ip', 'enable-arp-announce']) self.cli_set(path + ['ip', 'enable-arp-ignore']) @@ -671,6 +675,9 @@ class BasicInterfaceTest: tmp = read_file(f'{proc_base}/forwarding') self.assertEqual('0', tmp) + tmp = read_file(f'{proc_base}/bc_forwarding') + self.assertEqual('1', tmp) + tmp = read_file(f'{proc_base}/proxy_arp') self.assertEqual('1', tmp) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 37e6b9e7a..4de90e1ec 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -20,6 +20,7 @@ from glob import glob from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError from vyos.util import cmd sysfs_config = { @@ -56,11 +57,63 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_delete(['firewall']) self.cli_commit() + # Verify chains/sets are cleaned up from nftables + nftables_search = [ + ['set M_smoketest_mac'], + ['set N_smoketest_network'], + ['set P_smoketest_port'], + ['set D_smoketest_domain'], + ['set RECENT_smoketest_4'], + ['chain NAME_smoketest'] + ] + + self.verify_nftables(nftables_search, 'ip filter', inverse=True) + + def verify_nftables(self, nftables_search, table, inverse=False, args=''): + nftables_output = cmd(f'sudo nft {args} list table {table}') + + 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(not matched if inverse else matched, msg=search) + + def test_geoip(self): + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_commit() + + nftables_search = [ + ['ip saddr @GEOIP_CC_smoketest_1', 'drop'], + ['ip saddr != @GEOIP_CC_smoketest_2', 'return'] + ] + # -t prevents 1000+ GeoIP elements being returned + self.verify_nftables(nftables_search, 'ip filter', args='-t') + def test_groups(self): + hostmap_path = ['system', 'static-host-mapping', 'host-name'] + example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11'] + + self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5']) + for ips in example_org: + self.cli_set(hostmap_path + ['example.org', 'inet', ips]) + + self.cli_commit() + self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) + self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) + self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) @@ -68,41 +121,88 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) self.cli_commit() - nftables_search = [ ['iifname "eth0"', 'jump NAME_smoketest'], - ['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'th dport { 53, 123 }', 'return'], - ['ether saddr { 00:01:02:03:04:05 }', 'return'] + ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'return'], + ['elements = { 172.16.99.0/24 }'], + ['elements = { 53, 123 }'], + ['ether saddr @M_smoketest_mac', 'return'], + ['elements = { 00:01:02:03:04:05 }'], + ['set D_smoketest_domain'], + ['elements = { 192.0.2.5, 192.0.2.8,'], + ['192.0.2.10, 192.0.2.11 }'], + ['ip saddr @D_smoketest_domain', 'return'] ] + self.verify_nftables(nftables_search, 'ip filter') - nftables_output = cmd('sudo nft list table ip filter') + self.cli_delete(['system', 'static-host-mapping']) + self.cli_commit() - 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, msg=search) + def test_nested_groups(self): + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'port', '123']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'include', 'smoketest_port']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network1']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port1']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + # Test circular includes + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) + + nftables_search = [ + ['iifname "eth0"', 'jump NAME_smoketest'], + ['ip saddr @N_smoketest_network1', 'th dport @P_smoketest_port1', 'return'], + ['elements = { 172.16.99.0/24, 172.16.101.0/24 }'], + ['elements = { 53, 123 }'] + ] + + self.verify_nftables(nftables_search, 'ip filter') def test_basic_rules(self): self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'enable-default-log']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'log-level', 'debug']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'ttl', 'eq', '15']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'destination', 'port', '8888']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'log', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'log-level', 'err']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'tcp', 'flags', 'not', 'ack']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'ttl', 'gt', '102']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'protocol', 'tcp']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'destination', 'port', '22']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'limit', 'rate', '5/minute']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'log', 'disable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'protocol', 'tcp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'destination', 'port', '22']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'count', '10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'time', 'minute']) self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) @@ -110,27 +210,25 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['iifname "eth0"', 'jump NAME_smoketest'], - ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'], - ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'reject'], + ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[smoketest-1-A]" level debug', 'ip ttl 15','return'], + ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'log prefix "[smoketest-2-R]" level err', 'ip ttl > 102', 'reject'], ['tcp dport { 22 }', 'limit rate 5/minute', 'return'], - ['smoketest default-action', 'drop'] + ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'], + ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'] ] - nftables_output = cmd('sudo nft list table ip 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, msg=search) + self.verify_nftables(nftables_search, 'ip filter') def test_basic_rules_ipv6(self): self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'enable-default-log']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'source', 'address', '2002::1']) self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'destination', 'address', '2002::1:1']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'log', 'enable']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'log-level', 'crit']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'destination', 'port', '8888']) @@ -141,20 +239,12 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_search = [ ['iifname "eth0"', 'jump NAME6_v6-smoketest'], - ['saddr 2002::1', 'daddr 2002::1:1', 'return'], + ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[v6-smoketest-1-A]" level crit', 'return'], ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'], - ['smoketest default-action', 'drop'] + ['smoketest default-action', 'log prefix "[v6-smoketest-default-D]"', 'drop'] ] - nftables_output = cmd('sudo nft list table ip6 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, msg=search) + self.verify_nftables(nftables_search, 'ip6 filter') def test_state_policy(self): self.cli_set(['firewall', 'state-policy', 'established', 'action', 'accept']) @@ -173,6 +263,37 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): nftables_output = cmd(f'sudo nft list chain {table} {chain}') self.assertTrue('jump VYOS_STATE_POLICY' in nftables_output) + def test_state_and_status_rules(self): + self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'state', 'established', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'state', 'related', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'state', 'invalid', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'state', 'new', 'enable']) + + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'connection-status', 'nat', 'destination']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'state', 'new', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'state', 'established', 'enable']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'connection-status', 'nat', 'source']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump NAME_smoketest'], + ['ct state { established, related }', 'return'], + ['ct state { invalid }', 'reject'], + ['ct state { new }', 'ct status { dnat }', 'return'], + ['ct state { established, new }', 'ct status { snat }', 'return'], + ['smoketest default-action', 'drop'] + ] + + self.verify_nftables(nftables_search, 'ip filter') + def test_sysfs(self): for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 237abb487..cd3995ed9 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -49,7 +49,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): if not '.' in tmp: cls._members.append(tmp) - cls._options['bond0'] = [] + cls._options = {'bond0' : []} for member in cls._members: cls._options['bond0'].append(f'member interface {member}') cls._interfaces = list(cls._options) @@ -136,7 +136,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): def test_bonding_hash_policy(self): # Define available bonding hash policies - hash_policies = ['layer2', 'layer2+3', 'layer2+3', 'encap2+3', 'encap3+4'] + hash_policies = ['layer2', 'layer2+3', 'encap2+3', 'encap3+4'] for hash_policy in hash_policies: for interface in self._interfaces: for option in self._options.get(interface, []): @@ -151,6 +151,29 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split() self.assertEqual(defined_policy[0], hash_policy) + def test_bonding_mii_monitoring_interval(self): + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_commit() + + # verify default + for interface in self._interfaces: + tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split() + self.assertIn('100', tmp) + + mii_mon = '250' + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'mii-mon-interval', mii_mon]) + + self.cli_commit() + + # verify new CLI value + for interface in self._interfaces: + tmp = read_file(f'/sys/class/net/{interface}/bonding/miimon').split() + self.assertIn(mii_mon, tmp) + def test_bonding_multi_use_member(self): # Define available bonding hash policies for interface in ['bond10', 'bond20']: @@ -165,6 +188,46 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() + def test_bonding_source_interface(self): + # Re-use member interface that is already a source-interface + bond = 'bond99' + pppoe = 'pppoe98756' + member = next(iter(self._members)) + + self.cli_set(self._base_path + [bond, 'member', 'interface', member]) + self.cli_set(['interfaces', 'pppoe', pppoe, 'source-interface', member]) + + # check validate() - can not add interface to bond, it is the source-interface of ... + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['interfaces', 'pppoe', pppoe]) + self.cli_commit() + + # verify config + slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split() + self.assertIn(member, slaves) + + def test_bonding_source_bridge_interface(self): + # Re-use member interface that is already a source-interface + bond = 'bond1097' + bridge = 'br6327' + member = next(iter(self._members)) + + self.cli_set(self._base_path + [bond, 'member', 'interface', member]) + self.cli_set(['interfaces', 'bridge', bridge, 'member', 'interface', member]) + + # check validate() - can not add interface to bond, it is a member of bridge ... + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['interfaces', 'bridge', bridge]) + self.cli_commit() + + # verify config + slaves = read_file(f'/sys/class/net/{bond}/bonding/slaves').split() + self.assertIn(member, slaves) + def test_bonding_uniq_member_description(self): ethernet_path = ['interfaces', 'ethernet'] for interface in self._interfaces: diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index ca0ead9e8..664dc48bc 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -86,9 +86,83 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # validate member interface configuration for member in self._members: tmp = get_interface_config(member) + # verify member is assigned to the bridge + self.assertEqual(interface, tmp['master']) # Isolated must be enabled as configured above self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated']) + def test_igmp_querier_snooping(self): + # Add member interfaces to bridge + for interface in self._interfaces: + base = self._base_path + [interface] + + # assign members to bridge interface + for member in self._members: + base_member = base + ['member', 'interface', member] + self.cli_set(base_member) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP default configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '0') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # Enable IGMP snooping + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['igmp', 'snooping']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping configuration + # Verify IGMP default configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '1') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # Enable IGMP querieer + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['igmp', 'querier']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping & querier configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '1') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '1') + + # Disable IGMP + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_delete(base + ['igmp']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping & querier configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '0') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # validate member interface configuration + for member in self._members: + tmp = get_interface_config(member) + # verify member is assigned to the bridge + self.assertEqual(interface, tmp['master']) + def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority @@ -230,7 +304,7 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(self._base_path + [interface, 'member']) - def test_bridge_vlan_members(self): + def test_bridge_vif_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] for interface in self._interfaces: @@ -255,5 +329,34 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(['interfaces', 'ethernet', member, 'vif', vif]) self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}']) + def test_bridge_vif_s_vif_c_members(self): + # T2945: ensure that VIFs are not dropped from bridge + vifs = ['300', '400'] + vifc = ['301', '401'] + for interface in self._interfaces: + for member in self._members: + for vif_s in vifs: + for vif_c in vifc: + self.cli_set(['interfaces', 'ethernet', member, 'vif-s', vif_s, 'vif-c', vif_c]) + self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}']) + + self.cli_commit() + + # Verify config + for interface in self._interfaces: + for member in self._members: + for vif_s in vifs: + for vif_c in vifc: + # member interface must be assigned to the bridge + self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif_s}.{vif_c}')) + + # delete all members + for interface in self._interfaces: + for member in self._members: + for vif_s in vifs: + self.cli_delete(['interfaces', 'ethernet', member, 'vif-s', vif_s]) + for vif_c in vifc: + self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py new file mode 100755 index 000000000..9cbf104f0 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest + +class VTIInterfaceTest(BasicInterfaceTest.TestCase): + @classmethod + def setUpClass(cls): + cls._test_ip = True + cls._test_ipv6 = True + cls._test_mtu = True + cls._base_path = ['interfaces', 'vti'] + cls._interfaces = ['vti10', 'vti20', 'vti30'] + + # call base-classes classmethod + super(VTIInterfaceTest, cls).setUpClass() + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load_balancning_wan.py b/smoketest/scripts/cli/test_load_balancning_wan.py new file mode 100755 index 000000000..303dece86 --- /dev/null +++ b/smoketest/scripts/cli/test_load_balancning_wan.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 unittest +import time + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import call +from vyos.util import cmd + + +base_path = ['load-balancing'] + + +def create_netns(name): + return call(f'sudo ip netns add {name}') + +def create_veth_pair(local='veth0', peer='ceth0'): + return call(f'sudo ip link add {local} type veth peer name {peer}') + +def move_interface_to_netns(iface, netns_name): + return call(f'sudo ip link set {iface} netns {netns_name}') + +def rename_interface(iface, new_name): + return call(f'sudo ip link set {iface} name {new_name}') + +def cmd_in_netns(netns, cmd): + return call(f'sudo ip netns exec {netns} {cmd}') + +def delete_netns(name): + return call(f'sudo ip netns del {name}') + + +class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestLoadBalancingWan, 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): + self.cli_delete(base_path) + self.cli_commit() + + def test_table_routes(self): + + ns1 = 'ns201' + ns2 = 'ns202' + ns3 = 'ns203' + iface1 = 'eth201' + iface2 = 'eth202' + iface3 = 'eth203' + container_iface1 = 'ceth0' + container_iface2 = 'ceth1' + container_iface3 = 'ceth2' + + # Create network namespeces + create_netns(ns1) + create_netns(ns2) + create_netns(ns3) + create_veth_pair(iface1, container_iface1) + create_veth_pair(iface2, container_iface2) + create_veth_pair(iface3, container_iface3) + move_interface_to_netns(container_iface1, ns1) + move_interface_to_netns(container_iface2, ns2) + move_interface_to_netns(container_iface3, ns3) + call(f'sudo ip address add 203.0.113.10/24 dev {iface1}') + call(f'sudo ip address add 192.0.2.10/24 dev {iface2}') + call(f'sudo ip address add 198.51.100.10/24 dev {iface3}') + call(f'sudo ip link set dev {iface1} up') + call(f'sudo ip link set dev {iface2} up') + call(f'sudo ip link set dev {iface3} up') + cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0') + cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0') + cmd_in_netns(ns3, f'ip link set {container_iface3} name eth0') + cmd_in_netns(ns1, 'ip address add 203.0.113.1/24 dev eth0') + cmd_in_netns(ns2, 'ip address add 192.0.2.1/24 dev eth0') + cmd_in_netns(ns3, 'ip address add 198.51.100.1/24 dev eth0') + cmd_in_netns(ns1, 'ip link set dev eth0 up') + cmd_in_netns(ns2, 'ip link set dev eth0 up') + cmd_in_netns(ns3, 'ip link set dev eth0 up') + + # Set load-balancing configuration + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2']) + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1']) + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1']) + + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) + + + # commit changes + self.cli_commit() + + time.sleep(5) + # Check default routes in tables 201, 202 + # Expected values + original = 'default via 203.0.113.1 dev eth201' + tmp = cmd('sudo ip route show table 201') + self.assertEqual(tmp, original) + + original = 'default via 192.0.2.1 dev eth202' + tmp = cmd('sudo ip route show table 202') + self.assertEqual(tmp, original) + + # Delete veth interfaces and netns + for iface in [iface1, iface2]: + call(f'sudo ip link del dev {iface}') + + delete_netns(ns1) + delete_netns(ns2) + + def test_check_chains(self): + + ns1 = 'nsA' + ns2 = 'nsB' + ns3 = 'nsC' + iface1 = 'veth1' + iface2 = 'veth2' + iface3 = 'veth3' + container_iface1 = 'ceth0' + container_iface2 = 'ceth1' + container_iface3 = 'ceth2' + mangle_isp1 = """table ip mangle { + chain ISP_veth1 { + counter ct mark set 0xc9 + counter meta mark set 0xc9 + counter accept + } +}""" + mangle_isp2 = """table ip mangle { + chain ISP_veth2 { + counter ct mark set 0xca + counter meta mark set 0xca + counter accept + } +}""" + mangle_prerouting = """table ip mangle { + chain PREROUTING { + type filter hook prerouting priority mangle; policy accept; + counter jump WANLOADBALANCE_PRE + } +}""" + mangle_wanloadbalance_pre = """table ip mangle { + chain WANLOADBALANCE_PRE { + iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth1 + iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2 + iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark + } +}""" + nat_wanloadbalance = """table ip nat { + chain WANLOADBALANCE { + ct mark 0xc9 counter snat to 203.0.113.10 + ct mark 0xca counter snat to 192.0.2.10 + } +}""" + nat_vyos_pre_snat_hook = """table ip nat { + chain VYOS_PRE_SNAT_HOOK { + counter jump WANLOADBALANCE + return + } +}""" + + # Create network namespeces + create_netns(ns1) + create_netns(ns2) + create_netns(ns3) + create_veth_pair(iface1, container_iface1) + create_veth_pair(iface2, container_iface2) + create_veth_pair(iface3, container_iface3) + move_interface_to_netns(container_iface1, ns1) + move_interface_to_netns(container_iface2, ns2) + move_interface_to_netns(container_iface3, ns3) + call(f'sudo ip address add 203.0.113.10/24 dev {iface1}') + call(f'sudo ip address add 192.0.2.10/24 dev {iface2}') + call(f'sudo ip address add 198.51.100.10/24 dev {iface3}') + call(f'sudo ip link set dev {iface1} up') + call(f'sudo ip link set dev {iface2} up') + call(f'sudo ip link set dev {iface3} up') + cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0') + cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0') + cmd_in_netns(ns3, f'ip link set {container_iface3} name eth0') + cmd_in_netns(ns1, 'ip address add 203.0.113.1/24 dev eth0') + cmd_in_netns(ns2, 'ip address add 192.0.2.1/24 dev eth0') + cmd_in_netns(ns3, 'ip address add 198.51.100.1/24 dev eth0') + cmd_in_netns(ns1, 'ip link set dev eth0 up') + cmd_in_netns(ns2, 'ip link set dev eth0 up') + cmd_in_netns(ns3, 'ip link set dev eth0 up') + + # Set load-balancing configuration + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2']) + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1']) + self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'failure-count', '2']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'nexthop', '192.0.2.1']) + self.cli_set(base_path + ['wan', 'interface-health', iface2, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) + + # commit changes + self.cli_commit() + + time.sleep(5) + + # Check mangle chains + tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}') + self.assertEqual(tmp, mangle_isp1) + + tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}') + self.assertEqual(tmp, mangle_isp2) + + tmp = cmd(f'sudo nft -s list chain mangle PREROUTING') + self.assertEqual(tmp, mangle_prerouting) + + tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE') + self.assertEqual(tmp, mangle_wanloadbalance_pre) + + # Check nat chains + tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE') + self.assertEqual(tmp, nat_wanloadbalance) + + tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK') + self.assertEqual(tmp, nat_vyos_pre_snat_hook) + + # Delete veth interfaces and netns + for iface in [iface1, iface2]: + call(f'sudo ip link del dev {iface}') + + delete_netns(ns1) + delete_netns(ns2) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index e92123dbc..cba5ffdde 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -128,6 +128,27 @@ g6a75NnEXo0J6YLAOOxd8fD2/HidhbceCmTF+3msidIzCsBidBkgn6V5TXx2IyMS xGsJxVHfSKeooUQn6q76sg== """ +valid_update_cert = """ +MIICJTCCAcugAwIBAgIUZJqjNmPfVQwePjNFBtB6WI31ThMwCgYIKoZIzj0EAwIw +VzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv +bWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yMjA1 +MzExNTE3NDlaFw0yMzA1MzExNTE3NDlaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQI +DApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1Mx +EDAOBgNVBAMMB3Z5b3MuaW8wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQMe0h/ +3CdD8mEgy+klk55QfJ8R3ZycefxCn4abWjzTXz/TuCIxqb4wpRT8DZtIn4NRimFT +mODYdEDOYxFtZm37o3UwczAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAT +BgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUqH7KSZpzArpMFuxLXqI8e1QD +fBkwHwYDVR0jBBgwFoAUqH7KSZpzArpMFuxLXqI8e1QDfBkwCgYIKoZIzj0EAwID +SAAwRQIhAKofUgRtcUljmbubPF6sqHtn/3TRvuafl8VfPbk3s2bJAiBp3Q1AnU/O +i7t5FGhCgnv5m8DW2F3LZPCJdW4ELQ3d9A== +""" + +valid_update_private_key = """ +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvyODf22w/p7Zgfz9 +dyLIT09LqLOrUN6zbAecfukiiiyhRANCAAQMe0h/3CdD8mEgy+klk55QfJ8R3Zyc +efxCn4abWjzTXz/TuCIxqb4wpRT8DZtIn4NRimFTmODYdEDOYxFtZm37 +""" + class TestPKI(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -189,5 +210,41 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_certificate_in_use(self): + self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + self.cli_commit() + + self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_commit() + + self.cli_delete(base_path + ['certificate', 'smoketest']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(['service', 'https', 'certificates', 'certificate']) + + def test_certificate_https_update(self): + self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + self.cli_commit() + + self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_commit() + + cert_data = None + + with open('/etc/ssl/certs/smoketest.pem') as f: + cert_data = f.read() + + self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_commit() + + with open('/etc/ssl/certs/smoketest.pem') as f: + self.assertNotEqual(cert_data, f.read()) + + self.cli_delete(['service', 'https', 'certificates', 'certificate']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 3e8dd35ae..3d37d22ae 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -711,13 +711,20 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): large_community_list = 'bgp-large-community-123456' prefix_list = 'foo-pfx-list' - ipv6_nexthop = 'fe80::1' + ipv6_nexthop_address = 'fe80::1' local_pref = '300' metric = '50' peer = '2.3.4.5' + peerv6 = '2001:db8::1' tag = '6542' goto = '25' + ipv4_nexthop_address= '192.0.2.2' + ipv4_prefix_len= '18' + ipv6_prefix_len= '122' + ipv4_nexthop_type= 'blackhole' + ipv6_nexthop_type= 'blackhole' + test_data = { 'foo-map-bar' : { 'rule' : { @@ -785,7 +792,11 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): '30' : { 'action' : 'permit', 'match' : { - 'ipv6-nexthop' : ipv6_nexthop, + 'ipv6-nexthop-address' : ipv6_nexthop_address, + 'ipv6-nexthop-access-list' : access_list, + 'ipv6-nexthop-prefix-list' : prefix_list, + 'ipv6-nexthop-type' : ipv6_nexthop_type, + 'ipv6-address-pfx-len' : ipv6_prefix_len, 'large-community' : large_community_list, 'local-pref' : local_pref, 'metric': metric, @@ -793,6 +804,33 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'peer' : peer, }, }, + + '31' : { + 'action' : 'permit', + 'match' : { + 'peer' : peerv6, + }, + }, + + '40' : { + 'action' : 'permit', + 'match' : { + 'ip-nexthop-addr' : ipv4_nexthop_address, + 'ip-address-pfx-len' : ipv4_prefix_len, + }, + }, + '42' : { + 'action' : 'deny', + 'match' : { + 'ip-nexthop-plen' : ipv4_prefix_len, + }, + }, + '44' : { + 'action' : 'permit', + 'match' : { + 'ip-nexthop-type' : ipv4_nexthop_type, + }, + }, }, }, 'complicated-configuration' : { @@ -849,6 +887,35 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'evpn-vni' : '1234', }, }, + '20' : { + 'action' : 'permit', + 'set' : { + 'evpn-gateway-ipv4' : '192.0.2.99', + 'evpn-gateway-ipv6' : '2001:db8:f00::1', + }, + }, + }, + }, + 'relative-metric' : { + 'rule' : { + '10' : { + 'action' : 'permit', + 'match' : { + 'ip-nexthop-addr' : ipv4_nexthop_address, + }, + 'set' : { + 'metric' : '+10', + }, + }, + '20' : { + 'action' : 'permit', + 'match' : { + 'ip-nexthop-addr' : ipv4_nexthop_address, + }, + 'set' : { + 'metric' : '-20', + }, + }, }, }, } @@ -910,10 +977,18 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'access-list', rule_config['match']['ip-address-acl']]) if 'ip-address-pfx' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'prefix-list', rule_config['match']['ip-address-pfx']]) + if 'ip-address-pfx-len' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ip', 'address', 'prefix-len', rule_config['match']['ip-address-pfx-len']]) if 'ip-nexthop-acl' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'access-list', rule_config['match']['ip-nexthop-acl']]) if 'ip-nexthop-pfx' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'prefix-list', rule_config['match']['ip-nexthop-pfx']]) + if 'ip-nexthop-addr' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'address', rule_config['match']['ip-nexthop-addr']]) + if 'ip-nexthop-plen' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'prefix-len', rule_config['match']['ip-nexthop-plen']]) + if 'ip-nexthop-type' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ip', 'nexthop', 'type', rule_config['match']['ip-nexthop-type']]) if 'ip-route-source-acl' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'ip', 'route-source', 'access-list', rule_config['match']['ip-route-source-acl']]) if 'ip-route-source-pfx' in rule_config['match']: @@ -922,8 +997,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'access-list', rule_config['match']['ipv6-address-acl']]) if 'ipv6-address-pfx' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'prefix-list', rule_config['match']['ipv6-address-pfx']]) - if 'ipv6-nexthop' in rule_config['match']: - self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', rule_config['match']['ipv6-nexthop']]) + if 'ipv6-address-pfx-len' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'address', 'prefix-len', rule_config['match']['ipv6-address-pfx-len']]) + if 'ipv6-nexthop-address' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'address', rule_config['match']['ipv6-nexthop-address']]) + if 'ipv6-nexthop-access-list' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'access-list', rule_config['match']['ipv6-nexthop-access-list']]) + if 'ipv6-nexthop-prefix-list' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'prefix-list', rule_config['match']['ipv6-nexthop-prefix-list']]) + if 'ipv6-nexthop-type' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'ipv6', 'nexthop', 'type', rule_config['match']['ipv6-nexthop-type']]) if 'large-community' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'large-community', 'large-community-list', rule_config['match']['large-community']]) if 'local-pref' in rule_config['match']: @@ -996,6 +1079,10 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'set', 'tag', rule_config['set']['tag']]) if 'weight' in rule_config['set']: self.cli_set(path + ['rule', rule, 'set', 'weight', rule_config['set']['weight']]) + if 'evpn-gateway-ipv4' in rule_config['set']: + self.cli_set(path + ['rule', rule, 'set', 'evpn', 'gateway', 'ipv4', rule_config['set']['evpn-gateway-ipv4']]) + if 'evpn-gateway-ipv6' in rule_config['set']: + self.cli_set(path + ['rule', rule, 'set', 'evpn', 'gateway', 'ipv6', rule_config['set']['evpn-gateway-ipv6']]) self.cli_commit() @@ -1046,12 +1133,24 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'ip-address-pfx' in rule_config['match']: tmp = f'match ip address prefix-list {rule_config["match"]["ip-address-pfx"]}' self.assertIn(tmp, config) + if 'ip-address-pfx-len' in rule_config['match']: + tmp = f'match ip address prefix-len {rule_config["match"]["ip-address-pfx-len"]}' + self.assertIn(tmp, config) if 'ip-nexthop-acl' in rule_config['match']: tmp = f'match ip next-hop {rule_config["match"]["ip-nexthop-acl"]}' self.assertIn(tmp, config) if 'ip-nexthop-pfx' in rule_config['match']: tmp = f'match ip next-hop prefix-list {rule_config["match"]["ip-nexthop-pfx"]}' self.assertIn(tmp, config) + if 'ip-nexthop-addr' in rule_config['match']: + tmp = f'match ip next-hop address {rule_config["match"]["ip-nexthop-addr"]}' + self.assertIn(tmp, config) + if 'ip-nexthop-plen' in rule_config['match']: + tmp = f'match ip next-hop prefix-len {rule_config["match"]["ip-nexthop-plen"]}' + self.assertIn(tmp, config) + if 'ip-nexthop-type' in rule_config['match']: + tmp = f'match ip next-hop type {rule_config["match"]["ip-nexthop-type"]}' + self.assertIn(tmp, config) if 'ip-route-source-acl' in rule_config['match']: tmp = f'match ip route-source {rule_config["match"]["ip-route-source-acl"]}' self.assertIn(tmp, config) @@ -1064,8 +1163,20 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'ipv6-address-pfx' in rule_config['match']: tmp = f'match ipv6 address prefix-list {rule_config["match"]["ipv6-address-pfx"]}' self.assertIn(tmp, config) - if 'ipv6-nexthop' in rule_config['match']: - tmp = f'match ipv6 next-hop address {rule_config["match"]["ipv6-nexthop"]}' + if 'ipv6-address-pfx-len' in rule_config['match']: + tmp = f'match ipv6 address prefix-len {rule_config["match"]["ipv6-address-pfx-len"]}' + self.assertIn(tmp, config) + if 'ipv6-nexthop-address' in rule_config['match']: + tmp = f'match ipv6 next-hop address {rule_config["match"]["ipv6-nexthop-address"]}' + self.assertIn(tmp, config) + if 'ipv6-nexthop-access-list' in rule_config['match']: + tmp = f'match ipv6 next-hop {rule_config["match"]["ipv6-nexthop-access-list"]}' + self.assertIn(tmp, config) + if 'ipv6-nexthop-prefix-list' in rule_config['match']: + tmp = f'match ipv6 next-hop prefix-list {rule_config["match"]["ipv6-nexthop-prefix-list"]}' + self.assertIn(tmp, config) + if 'ipv6-nexthop-type' in rule_config['match']: + tmp = f'match ipv6 next-hop type {rule_config["match"]["ipv6-nexthop-type"]}' self.assertIn(tmp, config) if 'large-community' in rule_config['match']: tmp = f'match large-community {rule_config["match"]["large-community"]}' @@ -1155,6 +1266,10 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): tmp += 'tag ' + rule_config['set']['tag'] elif 'weight' in rule_config['set']: tmp += 'weight ' + rule_config['set']['weight'] + elif 'vpn-gateway-ipv4' in rule_config['set']: + tmp += 'evpn gateway ipv4 ' + rule_config['set']['vpn-gateway-ipv4'] + elif 'vpn-gateway-ipv6' in rule_config['set']: + tmp += 'evpn gateway ipv6 ' + rule_config['set']['vpn-gateway-ipv6'] self.assertIn(tmp, config) diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 9035f0832..534cfb082 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -23,44 +23,88 @@ from vyos.util import cmd mark = '100' table_mark_offset = 0x7fffffff table_id = '101' +interface = 'eth0' +interface_ip = '172.16.10.1/24' class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24']) - self.cli_set(['protocols', 'static', 'table', '101', 'route', '0.0.0.0/0', 'interface', 'eth0']) + @classmethod + def setUpClass(cls): + super(TestPolicyRoute, cls).setUpClass() + + cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip]) + cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface]) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip]) + cls.cli_delete(cls, ['protocols', 'static', 'table', table_id]) + + super(TestPolicyRoute, cls).tearDownClass() def tearDown(self): - self.cli_delete(['interfaces', 'ethernet', 'eth0']) - self.cli_delete(['protocols', 'static']) + self.cli_delete(['interfaces', 'ethernet', interface, 'policy']) self.cli_delete(['policy', 'route']) self.cli_delete(['policy', 'route6']) self.cli_commit() + nftables_search = [ + ['set N_smoketest_network'], + ['set N_smoketest_network1'], + ['chain VYOS_PBR_smoketest'] + ] + + self.verify_nftables(nftables_search, 'ip filter', inverse=True) + + def verify_nftables(self, nftables_search, table, inverse=False): + nftables_output = cmd(f'sudo nft list table {table}') + + 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(not matched if inverse else matched, msg=search) + + def test_pbr_group(self): + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network']) + + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'group', 'network-group', 'smoketest_network1']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark]) + + self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + [f'iifname "{interface}"','jump VYOS_PBR_smoketest'], + ['ip daddr @N_smoketest_network1', 'ip saddr @N_smoketest_network'], + ] + + self.verify_nftables(nftables_search, 'ip mangle') + + self.cli_delete(['firewall']) + def test_pbr_mark(self): self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark]) - self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest']) + self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) self.cli_commit() mark_hex = "{0:#010x}".format(int(mark)) nftables_search = [ - ['iifname "eth0"', 'jump VYOS_PBR_smoketest'], + [f'iifname "{interface}"','jump VYOS_PBR_smoketest'], ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex], ] - nftables_output = cmd('sudo nft list table ip mangle') - - 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) + self.verify_nftables(nftables_search, 'ip mangle') def test_pbr_table(self): self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp']) @@ -72,8 +116,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'table', table_id]) - self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest']) - self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route6', 'smoketest6']) + self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) + self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route6', 'smoketest6']) self.cli_commit() @@ -82,36 +126,20 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): # IPv4 nftables_search = [ - ['iifname "eth0"', 'jump VYOS_PBR_smoketest'], + [f'iifname "{interface}"', 'jump VYOS_PBR_smoketest'], ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'meta mark set ' + mark_hex] ] - nftables_output = cmd('sudo nft list table ip mangle') - - 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) + self.verify_nftables(nftables_search, 'ip mangle') # IPv6 nftables6_search = [ - ['iifname "eth0"', 'jump VYOS_PBR6_smoketest'], + [f'iifname "{interface}"', 'jump VYOS_PBR6_smoketest'], ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'meta mark set ' + mark_hex] ] - nftables6_output = cmd('sudo nft list table ip6 mangle') - - for search in nftables6_search: - matched = False - for line in nftables6_output.split("\n"): - if all(item in line for item in search): - matched = True - break - self.assertTrue(matched) + self.verify_nftables(nftables6_search, 'ip6 mangle') # IP rule fwmark -> table diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 6f92457b2..9c0c93779 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -887,6 +887,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): remote_asn = str(int(ASN) + 150) neighbor = '192.0.2.1' peer_group = 'bar' + interface = 'eth0' self.cli_set(base_path + ['local-as', ASN]) self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn]) @@ -898,6 +899,20 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(base_path + ['neighbor', neighbor, 'remote-as']) + # re-test with interface based peer-group + self.cli_set(base_path + ['neighbor', interface, 'interface', 'peer-group', peer_group]) + self.cli_set(base_path + ['neighbor', interface, 'interface', 'remote-as', 'external']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['neighbor', interface, 'interface', 'remote-as']) + + # re-test with interface based v6only peer-group + self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'peer-group', peer_group]) + self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as', 'external']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as']) + self.cli_commit() frrconfig = self.getFRRconfig(f'router bgp {ASN}') diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 80d4e79f9..11385adb5 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -31,26 +31,38 @@ route_map = 'FooBar123' base_path = ['protocols', 'rip'] class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['policy', 'access-list', acl_in, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'access-list', acl_in, 'rule', '10', 'source', 'any']) - self.cli_set(['policy', 'access-list', acl_in, 'rule', '10', 'destination', 'any']) - self.cli_set(['policy', 'access-list', acl_out, 'rule', '20', 'action', 'deny']) - self.cli_set(['policy', 'access-list', acl_out, 'rule', '20', 'source', 'any']) - self.cli_set(['policy', 'access-list', acl_out, 'rule', '20', 'destination', 'any']) - self.cli_set(['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'prefix', '192.0.2.0/24']) - self.cli_set(['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'action', 'deny']) - self.cli_set(['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'prefix', '192.0.2.0/24']) - self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + @classmethod + def setUpClass(cls): + super(TestProtocolsRIP, 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) + + cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'source', 'any']) + cls.cli_set(cls, ['policy', 'access-list', acl_in, 'rule', '10', 'destination', 'any']) + cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'action', 'deny']) + cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'source', 'any']) + cls.cli_set(cls, ['policy', 'access-list', acl_out, 'rule', '20', 'destination', 'any']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_in, 'rule', '100', 'prefix', '192.0.2.0/24']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'action', 'deny']) + cls.cli_set(cls, ['policy', 'prefix-list', prefix_list_out, 'rule', '200', 'prefix', '192.0.2.0/24']) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['policy', 'access-list', acl_in]) + cls.cli_delete(cls, ['policy', 'access-list', acl_out]) + cls.cli_delete(cls, ['policy', 'prefix-list', prefix_list_in]) + cls.cli_delete(cls, ['policy', 'prefix-list', prefix_list_out]) + cls.cli_delete(cls, ['policy', 'route-map', route_map]) + + super(TestProtocolsRIP, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) - self.cli_delete(['policy', 'access-list', acl_in]) - self.cli_delete(['policy', 'access-list', acl_out]) - self.cli_delete(['policy', 'prefix-list', prefix_list_in]) - self.cli_delete(['policy', 'prefix-list', prefix_list_out]) - self.cli_delete(['policy', 'route-map', route_map]) self.cli_commit() # Check for running process @@ -146,5 +158,25 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): frrconfig = self.getFRRconfig(zebra_route_map) self.assertNotIn(zebra_route_map, frrconfig) + def test_rip_03_version(self): + rx_version = '1' + tx_version = '2' + interface = 'eth0' + + self.cli_set(base_path + ['version', tx_version]) + self.cli_set(base_path + ['interface', interface, 'send', 'version', tx_version]) + self.cli_set(base_path + ['interface', interface, 'receive', 'version', rx_version]) + + # commit changes + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('router rip') + self.assertIn(f'version {tx_version}', frrconfig) + + frrconfig = self.getFRRconfig(f'interface {interface}') + self.assertIn(f' ip rip receive version {rx_version}', frrconfig) + self.assertIn(f' ip rip send version {tx_version}', frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 5929f8cba..65b676451 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2022 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 @@ -39,7 +39,18 @@ def get_config_value(key, file=CONFIG_FILE): return tmp[0] class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestServicePowerDNS, 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 DNS forwarding configuration self.cli_delete(base_path) self.cli_commit() @@ -100,9 +111,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('serve-rfc1918') self.assertEqual(tmp, 'yes') - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_dnssec(self): # DNSSEC option testing @@ -121,9 +129,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('dnssec') self.assertEqual(tmp, option) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_external_nameserver(self): # Externe Domain Name Servers (DNS) addresses @@ -147,9 +152,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('export-etc-hosts') self.assertEqual(tmp, 'yes') - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_domain_forwarding(self): for network in allow_from: self.cli_set(base_path + ['allow-from', network]) @@ -186,9 +188,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): if domain == domains[1]: self.assertIn(f'addNTA("{domain}", "static")', hosts_conf) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_no_rfc1918_forwarding(self): for network in allow_from: self.cli_set(base_path + ['allow-from', network]) @@ -204,9 +203,26 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('serve-rfc1918') self.assertEqual(tmp, 'no') - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dns64(self): + dns_prefix = '64:ff9b::/96' + + for network in allow_from: + self.cli_set(base_path + ['allow-from', network]) + for address in listen_adress: + self.cli_set(base_path + ['listen-address', address]) + + # Check dns64-prefix - must be prefix /96 + self.cli_set(base_path + ['dns64-prefix', '2001:db8:aabb::/64']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['dns64-prefix', dns_prefix]) + + # commit changes + self.cli_commit() + + # verify dns64-prefix configuration + tmp = get_config_value('dns64-prefix') + self.assertEqual(tmp, dns_prefix) if __name__ == '__main__': unittest.main(verbosity=2) - diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py index 09937513e..f9d875e83 100755 --- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -39,10 +39,10 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_01_basic_config(self): - self.cli_set(base_path + ['authentication', 'organization', org]) - self.cli_set(base_path + ['authentication', 'token', token]) - self.cli_set(base_path + ['port', port]) - self.cli_set(base_path + ['url', url]) + self.cli_set(base_path + ['influxdb', 'authentication', 'organization', org]) + self.cli_set(base_path + ['influxdb', 'authentication', 'token', token]) + self.cli_set(base_path + ['influxdb', 'port', port]) + self.cli_set(base_path + ['influxdb', 'url', url]) # commit changes self.cli_commit() diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 4875fb5d1..1168c05cd 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -17,6 +17,7 @@ import re import unittest +from vyos.configsession import ConfigSessionError from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import read_file @@ -93,6 +94,7 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): def test_dns(self): nameserver = ['2001:db8::1', '2001:db8::2'] dnssl = ['vyos.net', 'vyos.io'] + ns_lifetime = '599' self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) self.cli_set(base_path + ['other-config-flag']) @@ -102,6 +104,14 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): for sl in dnssl: self.cli_set(base_path + ['dnssl', sl]) + self.cli_set(base_path + ['name-server-lifetime', ns_lifetime]) + # The value, if not 0, must be at least interval max (defaults to 600). + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + ns_lifetime = '600' + self.cli_set(base_path + ['name-server-lifetime', ns_lifetime]) + # commit changes self.cli_commit() @@ -110,8 +120,12 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = 'RDNSS ' + ' '.join(nameserver) + ' {' self.assertIn(tmp, config) + tmp = f'AdvRDNSSLifetime {ns_lifetime};' + self.assertIn(tmp, config) + tmp = 'DNSSL ' + ' '.join(dnssl) + ' {' self.assertIn(tmp, config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 77ad5bc0d..0b029dd00 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -213,5 +213,54 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): usernames = [x[0] for x in getpwall()] self.assertNotIn(test_user, usernames) + def test_ssh_dynamic_protection(self): + # check sshguard service + + SSHGUARD_CONFIG = '/etc/sshguard/sshguard.conf' + SSHGUARD_WHITELIST = '/etc/sshguard/whitelist' + SSHGUARD_PROCESS = 'sshguard' + block_time = '123' + detect_time = '1804' + port = '22' + threshold = '10' + allow_list = ['192.0.2.0/24', '2001:db8::/48'] + + self.cli_set(base_path + ['dynamic-protection', 'block-time', block_time]) + self.cli_set(base_path + ['dynamic-protection', 'detect-time', detect_time]) + self.cli_set(base_path + ['dynamic-protection', 'threshold', threshold]) + for allow in allow_list: + self.cli_set(base_path + ['dynamic-protection', 'allow-from', allow]) + + # commit changes + self.cli_commit() + + # Check configured port + tmp = get_config_value('Port') + self.assertIn(port, tmp) + + # Check sshgurad service + self.assertTrue(process_named_running(SSHGUARD_PROCESS)) + + sshguard_lines = [ + f'THRESHOLD={threshold}', + f'BLOCK_TIME={block_time}', + f'DETECTION_TIME={detect_time}' + ] + + tmp_sshguard_conf = read_file(SSHGUARD_CONFIG) + for line in sshguard_lines: + self.assertIn(line, tmp_sshguard_conf) + + tmp_whitelist_conf = read_file(SSHGUARD_WHITELIST) + for allow in allow_list: + self.assertIn(allow, tmp_whitelist_conf) + + # Delete service ssh dynamic-protection + # but not service ssh itself + self.cli_delete(base_path + ['dynamic-protection']) + self.cli_commit() + + self.assertFalse(process_named_running(SSHGUARD_PROCESS)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py index 5a73ebc7d..a6eef3fb6 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -20,6 +20,8 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section +from vyos.template import bracketize_ipv6 +from vyos.template import is_ipv6 from vyos.util import cmd from vyos.util import process_named_running from vyos.util import read_file @@ -103,14 +105,12 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): agent_address = '192.0.2.2' sflow_server = { - '1.2.3.4' : { - }, - '5.6.7.8' : { - 'port' : '6000' - } + '1.2.3.4' : { }, + '5.6.7.8' : { 'port' : '6000' }, } self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) self.cli_set(base_path + ['disable-imt']) # You need to configure at least one interface for flow-accounting @@ -155,6 +155,54 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'dummy', dummy_if]) + def test_sflow_ipv6(self): + sampling_rate = '100' + sflow_server = { + '2001:db8::1' : { }, + '2001:db8::2' : { 'port' : '6000' }, + } + + self.cli_set(base_path + ['disable-imt']) + + # You need to configure at least one interface for flow-accounting + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for interface in Section.interfaces('ethernet'): + self.cli_set(base_path + ['interface', interface]) + + + # You need to configure at least one sFlow or NetFlow protocol, or not + # set "disable-imt" for flow-accounting + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) + for server, server_config in sflow_server.items(): + self.cli_set(base_path + ['sflow', 'server', server]) + if 'port' in server_config: + self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) + + # commit changes + self.cli_commit() + + uacctd = read_file(uacctd_conf) + + # when 'disable-imt' is not configured on the CLI it must be present + self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) + self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) + self.assertNotIn(f'plugins: memory', uacctd) + + for server, server_config in sflow_server.items(): + tmp_srv = server + if is_ipv6(tmp_srv): + tmp_srv = tmp_srv.replace(':', '.') + + if 'port' in server_config: + self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) + else: + self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd) + self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd) + def test_netflow(self): engine_id = '33' max_flows = '667' @@ -173,14 +221,13 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): tmo_udp = '10' netflow_server = { - '11.22.33.44' : { - }, - '55.66.77.88' : { - 'port' : '6000' - } + '11.22.33.44' : { }, + '55.66.77.88' : { 'port' : '6000' }, + '2001:db8::1' : { }, } self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) + self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) for interface in Section.interfaces('ethernet'): self.cli_set(base_path + ['interface', interface]) @@ -217,23 +264,30 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): tmp = [] for server, server_config in netflow_server.items(): - tmp.append(f'nfprobe[nf_{server}]') + tmp_srv = server + if is_ipv6(tmp_srv): + tmp_srv = tmp_srv.replace(':', '.') + tmp.append(f'nfprobe[nf_{tmp_srv}]') tmp.append('memory') self.assertIn('plugins: ' + ','.join(tmp), uacctd) for server, server_config in netflow_server.items(): - self.assertIn(f'nfprobe_engine[nf_{server}]: {engine_id}', uacctd) - self.assertIn(f'nfprobe_maxflows[nf_{server}]: {max_flows}', uacctd) - self.assertIn(f'sampling_rate[nf_{server}]: {sampling_rate}', uacctd) - self.assertIn(f'nfprobe_source_ip[nf_{server}]: {source_address}', uacctd) - self.assertIn(f'nfprobe_version[nf_{server}]: {version}', uacctd) + tmp_srv = server + if is_ipv6(tmp_srv): + tmp_srv = tmp_srv.replace(':', '.') + + self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd) + self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd) + self.assertIn(f'sampling_rate[nf_{tmp_srv}]: {sampling_rate}', uacctd) + self.assertIn(f'nfprobe_source_ip[nf_{tmp_srv}]: {source_address}', uacctd) + self.assertIn(f'nfprobe_version[nf_{tmp_srv}]: {version}', uacctd) if 'port' in server_config: - self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}', uacctd) + self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) else: - self.assertIn(f'nfprobe_receiver[nf_{server}]: {server}:2055', uacctd) + self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}:2055', uacctd) - self.assertIn(f'nfprobe_timeouts[nf_{server}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd) + self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd) self.cli_delete(['interfaces', 'dummy', dummy_if]) diff --git a/smoketest/scripts/cli/test_system_frr.py b/smoketest/scripts/cli/test_system_frr.py new file mode 100755 index 000000000..331133ed4 --- /dev/null +++ b/smoketest/scripts/cli/test_system_frr.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 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.util import read_file + +config_file = '/etc/frr/daemons' +base_path = ['system', 'frr'] + + +def daemons_config_parse(daemons_config): + # create regex for parsing daemons options + regex_daemon_config = re.compile( + r'^(?P<daemon_name>\w+)_options="(?P<daemon_options>.*)"$', re.M) + # create empty dict for config + daemons_config_dict = {} + # fill dictionary with actual config + for daemon in regex_daemon_config.finditer(daemons_config): + daemon_name = daemon.group('daemon_name') + daemon_options = daemon.group('daemon_options') + daemons_config_dict[daemon_name] = daemon_options + + # return daemons config + return (daemons_config_dict) + + +class TestSystemFRR(VyOSUnitTestSHIM.TestCase): + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_frr_snmp_multipledaemons(self): + # test SNMP integration for multiple daemons + test_daemon_names = ['ospfd', 'bgpd'] + for test_daemon_name in test_daemon_names: + self.cli_set(base_path + ['snmp', test_daemon_name]) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex for matching SNMP integration + regex_snmp = re.compile(r'^.* -M snmp.*$') + for (daemon_name, daemon_options) in daemons_config_dict.items(): + snmp_enabled = regex_snmp.match(daemon_options) + if daemon_name in test_daemon_names: + self.assertTrue(snmp_enabled) + else: + self.assertFalse(snmp_enabled) + + def test_frr_snmp_addandremove(self): + # test enabling and disabling of SNMP integration + test_daemon_names = ['ospfd', 'bgpd'] + for test_daemon_name in test_daemon_names: + self.cli_set(base_path + ['snmp', test_daemon_name]) + self.cli_commit() + + self.cli_delete(base_path) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex for matching SNMP integration + regex_snmp = re.compile(r'^.* -M snmp.*$') + for test_daemon_name in test_daemon_names: + snmp_enabled = regex_snmp.match( + daemons_config_dict[test_daemon_name]) + self.assertFalse(snmp_enabled) + + def test_frr_snmp_empty(self): + # test empty config section + self.cli_set(base_path + ['snmp']) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex for matching SNMP integration + regex_snmp = re.compile(r'^.* -M snmp.*$') + for daemon_options in daemons_config_dict.values(): + snmp_enabled = regex_snmp.match(daemon_options) + self.assertFalse(snmp_enabled) + + def test_frr_bmp(self): + # test BMP + self.cli_set(base_path + ['bmp']) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex + regex_bmp = re.compile(r'^.* -M bmp.*$') + bmp_enabled = regex_bmp.match(daemons_config_dict['bgpd']) + self.assertTrue(bmp_enabled) + + def test_frr_irdp(self): + # test IRDP + self.cli_set(base_path + ['irdp']) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex + regex_irdp = re.compile(r'^.* -M irdp.*$') + irdp_enabled = regex_irdp.match(daemons_config_dict['zebra']) + self.assertTrue(irdp_enabled) + + def test_frr_bmpandsnmp(self): + # test empty config section + self.cli_set(base_path + ['bmp']) + self.cli_set(base_path + ['snmp', 'bgpd']) + self.cli_commit() + + # read the config file and check content + daemons_config = read_file(config_file) + daemons_config_dict = daemons_config_parse(daemons_config) + # prepare regex + regex_snmp = re.compile(r'^.* -M bmp.*$') + regex_snmp = re.compile(r'^.* -M snmp.*$') + bmp_enabled = regex_snmp.match(daemons_config_dict['bgpd']) + snmp_enabled = regex_snmp.match(daemons_config_dict['bgpd']) + self.assertTrue(bmp_enabled) + self.assertTrue(snmp_enabled) + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py index 83df9d99e..f71ef5b3f 100755 --- a/smoketest/scripts/cli/test_system_ip.py +++ b/smoketest/scripts/cli/test_system_ip.py @@ -28,7 +28,7 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): def test_system_ip_forwarding(self): # Test if IPv4 forwarding can be disabled globally, default is '1' - # which means forwearding enabled + # which means forwarding enabled all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding' self.assertEqual(read_file(all_forwarding), '1') @@ -37,6 +37,17 @@ class TestSystemIP(VyOSUnitTestSHIM.TestCase): self.assertEqual(read_file(all_forwarding), '0') + def test_system_ip_directed_broadcast_forwarding(self): + # Test if IPv4 directed broadcast forwarding can be disabled globally, + # default is '1' which means forwarding enabled + bc_forwarding = '/proc/sys/net/ipv4/conf/all/bc_forwarding' + self.assertEqual(read_file(bc_forwarding), '1') + + self.cli_set(base_path + ['disable-directed-broadcast']) + self.cli_commit() + + self.assertEqual(read_file(bc_forwarding), '0') + def test_system_ip_multipath(self): # Test IPv4 multipathing options, options default to off -> '0' use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh' diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index e2821687c..a0806acf0 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -108,5 +108,22 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for listen in listen_address: self.assertIn(f'interface listen {listen}', config) + def test_03_ntp_interface(self): + interfaces = ['eth0', 'eth1'] + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + servers = ['time1.vyos.net', 'time2.vyos.net'] + for server in servers: + self.cli_set(base_path + ['server', server]) + + self.cli_commit() + + # Check generated client address configuration + config = read_file(NTP_CONF) + self.assertIn('interface ignore wildcard', config) + for interface in interfaces: + self.assertIn(f'interface listen {interface}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index 24673278b..f58920b5b 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -17,7 +17,8 @@ import unittest from base_accel_ppp_test import BasicAccelPPPTest -from vyos.util import cmd +from vyos.util import read_file + pki_path = ['pki'] cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' @@ -40,6 +41,7 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): self.cli_set(pki_path + ['ca', 'sstp', 'certificate', cert_data]) self.cli_set(pki_path + ['certificate', 'sstp', 'certificate', cert_data]) self.cli_set(pki_path + ['certificate', 'sstp', 'private', 'key', key_data]) + # SSL is mandatory self.set(['ssl', 'ca-certificate', 'sstp']) self.set(['ssl', 'certificate', 'sstp']) @@ -47,5 +49,15 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): super().basic_config() + def test_accel_local_authentication(self): + # Change default port + port = '8443' + self.set(['port', port]) + + super().test_accel_local_authentication() + + config = read_file(self._config_file) + self.assertIn(f'port={port}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index ff18f7261..176c095fb 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -127,6 +127,9 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): for vrf in vrfs: # Ensure VRF was created self.assertIn(vrf, interfaces()) + # Verify IP forwarding is 1 (enabled) + self.assertEqual(read_file(f'/proc/sys/net/ipv4/conf/{vrf}/forwarding'), '1') + self.assertEqual(read_file(f'/proc/sys/net/ipv6/conf/{vrf}/forwarding'), '1') # Test for proper loopback IP assignment for addr in loopbacks: self.assertTrue(is_intf_addr_assigned(vrf, addr)) @@ -267,5 +270,26 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'dummy', interface]) self.cli_commit() + def test_vrf_disable_forwarding(self): + table = '2000' + for vrf in vrfs: + base = base_path + ['name', vrf] + self.cli_set(base + ['table', table]) + self.cli_set(base + ['ip', 'disable-forwarding']) + self.cli_set(base + ['ipv6', 'disable-forwarding']) + table = str(int(table) + 1) + + # commit changes + self.cli_commit() + + # Verify VRF configuration + loopbacks = ['127.0.0.1', '::1'] + for vrf in vrfs: + # Ensure VRF was created + self.assertIn(vrf, interfaces()) + # Verify IP forwarding is 0 (disabled) + self.assertEqual(read_file(f'/proc/sys/net/ipv4/conf/{vrf}/forwarding'), '0') + self.assertEqual(read_file(f'/proc/sys/net/ipv6/conf/{vrf}/forwarding'), '0') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/completion/list_bgp_neighbors.sh b/src/completion/list_bgp_neighbors.sh index f74f102ef..869a7ab0a 100755 --- a/src/completion/list_bgp_neighbors.sh +++ b/src/completion/list_bgp_neighbors.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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,19 +18,21 @@ ipv4=0 ipv6=0 +vrf="" while [[ "$#" -gt 0 ]]; do case $1 in -4|--ipv4) ipv4=1 ;; -6|--ipv6) ipv6=1 ;; -b|--both) ipv4=1; ipv6=1 ;; + --vrf) vrf="vrf name $2"; shift ;; *) echo "Unknown parameter passed: $1" ;; esac shift done declare -a vals -eval "vals=($(cli-shell-api listActiveNodes protocols bgp neighbor))" +eval "vals=($(cli-shell-api listActiveNodes $vrf protocols bgp neighbor))" if [ $ipv4 -eq 1 ] && [ $ipv6 -eq 1 ]; then echo -n '<x.x.x.x>' '<h:h:h:h:h:h:h:h>' ${vals[@]} @@ -54,9 +56,10 @@ elif [ $ipv6 -eq 1 ] ; then done else echo "Usage:" - echo "-4|--ipv4 list only IPv4 peers" - echo "-6|--ipv6 list only IPv6 peers" - echo "--both list both IP4 and IPv6 peers" + echo "-4|--ipv4 list only IPv4 peers" + echo "-6|--ipv6 list only IPv6 peers" + echo "--both list both IP4 and IPv6 peers" + echo "--vrf <name> apply command to given VRF (optional)" echo "" exit 1 fi diff --git a/src/completion/list_openconnect_users.py b/src/completion/list_openconnect_users.py new file mode 100755 index 000000000..a266fd893 --- /dev/null +++ b/src/completion/list_openconnect_users.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2022 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/>. + +from vyos.config import Config +from vyos.util import dict_search + +def get_user_from_ocserv(): + config = Config() + base = ['vpn', 'openconnect', 'authentication', 'local-users', 'username'] + openconnect = config.get_config_dict(base, effective=True, key_mangling=('-', '_')) + users = [] + try: + for user in (dict_search('username', openconnect) or []): + users.append(user) + except: + pass + return users + +if __name__ == "__main__": + users = [] + users = get_user_from_ocserv() + print(" ".join(users)) + diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 311e01529..c4b2bb488 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -116,6 +116,7 @@ def generate(conntrack): return None def apply(conntrack): + systemd_service = 'conntrackd.service' if not conntrack: # Failover mechanism daemon should be indicated that it no longer needs # to execute conntrackd actions on transition. This is only required @@ -123,7 +124,7 @@ def apply(conntrack): if process_named_running('conntrackd'): resync_vrrp() - call('systemctl stop conntrackd.service') + call(f'systemctl stop {systemd_service}') return None # Failover mechanism daemon should be indicated that it needs to execute @@ -132,7 +133,7 @@ def apply(conntrack): if not process_named_running('conntrackd'): resync_vrrp() - call('systemctl restart conntrackd.service') + call(f'systemctl reload-or-restart {systemd_service}') return None if __name__ == '__main__': diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 7e1dc5911..ac3dc536b 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -15,13 +15,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import json from ipaddress import ip_address from ipaddress import ip_network from time import sleep from json import dumps as json_write +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed @@ -90,10 +90,10 @@ def get_config(config=None): container['name'][name] = dict_merge(default_values, container['name'][name]) # Delete container network, delete containers - tmp = node_changed(conf, base + ['container', 'network']) + tmp = node_changed(conf, base + ['network']) if tmp: container.update({'network_remove' : tmp}) - tmp = node_changed(conf, base + ['container', 'name']) + tmp = node_changed(conf, base + ['name']) if tmp: container.update({'container_remove' : tmp}) return container @@ -110,15 +110,21 @@ def verify(container): if 'image' not in container_config: raise ConfigError(f'Container image for "{name}" is mandatory!') - # verify container image exists locally - image = container_config['image'] - # Check if requested container image exists locally. If it does not - # exist locally - inform the user. + # exist locally - inform the user. This is required as there is a + # shared container image storage accross all VyOS images. A user can + # delete a container image from the system, boot into another version + # of VyOS and then it would fail to boot. This is to prevent any + # configuration error when container images are deleted from the + # global storage. A per image local storage would be a super waste + # of diskspace as there will be a full copy (up tu several GB/image) + # on upgrade. This is the "cheapest" and fastest solution in terms + # of image upgrade and deletion. + image = container_config['image'] if run(f'podman image exists {image}') != 0: - raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\ - f'locally.\nPlease use "add container image {image}" to add it '\ - 'to the system!') + Warning(f'Image "{image}" used in contianer "{name}" does not exist '\ + f'locally. Please use "add container image {image}" to add it '\ + f'to the system! Container "{name}" will not be started!') if 'network' in container_config: if len(container_config['network']) > 1: @@ -126,7 +132,7 @@ def verify(container): # Check if the specified container network exists network_name = list(container_config['network'])[0] - if network_name not in container['network']: + if network_name not in container.get('network', {}): raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: @@ -264,12 +270,13 @@ def apply(container): # Option "--force" allows to delete containers with any status if 'container_remove' in container: for name in container['container_remove']: - call(f'podman stop {name}') + call(f'podman stop --time 3 {name}') call(f'podman rm --force {name}') # Delete old networks if needed if 'network_remove' in container: for network in container['network_remove']: + call(f'podman network rm {network}') tmp = f'/etc/cni/net.d/{network}.conflist' if os.path.exists(tmp): os.unlink(tmp) @@ -279,11 +286,16 @@ def apply(container): for name, container_config in container['name'].items(): image = container_config['image'] + if run(f'podman image exists {image}') != 0: + # container image does not exist locally - user already got + # informed by a WARNING in verfiy() - bail out early + continue + if 'disable' in container_config: # check if there is a container by that name running tmp = _cmd('podman ps -a --format "{{.Names}}"') if name in tmp: - _cmd(f'podman stop {name}') + _cmd(f'podman stop --time 3 {name}') _cmd(f'podman rm --force {name}') continue diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index f1c2d1f43..d0d87d73e 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -98,6 +98,9 @@ def get_config(config=None): dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) continue + if subnode == 'any': + subnode = '*' + for address in rdata['address']: zone['records'].append({ 'name': subnode, @@ -263,6 +266,12 @@ def verify(dns): if 'server' not in dns['domain'][domain]: raise ConfigError(f'No server configured for domain {domain}!') + if 'dns64_prefix' in dns: + dns_prefix = dns['dns64_prefix'].split('/')[1] + # RFC 6147 requires prefix /96 + if int(dns_prefix) != 96: + raise ConfigError('DNS 6to4 prefix must be of length /96') + if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']: for error in dns['authoritative_zone_errors']: print(error) diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py index 9a5d278e9..ab1c69259 100755 --- a/src/conf_mode/firewall-interface.py +++ b/src/conf_mode/firewall-interface.py @@ -64,6 +64,11 @@ def get_config(config=None): return if_firewall +def verify_chain(table, chain): + # Verify firewall applied + code = run(f'nft list chain {table} {chain}') + return code == 0 + def verify(if_firewall): # bail out early - looks like removal from running config if not if_firewall: @@ -80,6 +85,9 @@ def verify(if_firewall): if name not in if_firewall['firewall']['name']: raise ConfigError(f'Invalid firewall name "{name}"') + if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'): + raise ConfigError('Firewall did not apply') + if 'ipv6_name' in if_firewall[direction]: name = if_firewall[direction]['ipv6_name'] @@ -89,6 +97,9 @@ def verify(if_firewall): if name not in if_firewall['firewall']['ipv6_name']: raise ConfigError(f'Invalid firewall ipv6-name "{name}"') + if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'): + raise ConfigError('Firewall did not apply') + return None def generate(if_firewall): diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 6924bf555..07eca722f 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -26,9 +26,17 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdiff import get_config_diff, Diff +from vyos.firewall import geoip_update +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements from vyos.template import render +from vyos.util import call from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive from vyos.util import process_named_running from vyos.util import run from vyos.xml import defaults @@ -79,10 +87,26 @@ nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] valid_groups = [ 'address_group', + 'domain_group', 'network_group', 'port_group' ] +nested_group_types = [ + 'address_group', 'network_group', 'mac_group', + 'port_group', 'ipv6_address_group', 'ipv6_network_group' +] + +group_set_prefix = { + 'A_': 'address_group', + 'A6_': 'ipv6_address_group', + 'D_': 'domain_group', + 'M_': 'mac_group', + 'N_': 'network_group', + 'N6_': 'ipv6_network_group', + 'P_': 'port_group' +} + snmp_change_type = { 'unknown': 0, 'add': 1, @@ -138,6 +162,40 @@ def get_firewall_zones(conf): return {'name': used_v4, 'ipv6_name': used_v6} +def geoip_updated(conf, firewall): + diff = get_config_diff(conf) + node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) + + out = { + 'name': [], + 'ipv6_name': [], + 'deleted_name': [], + 'deleted_ipv6_name': [] + } + updated = False + + for key, path in dict_search_recursive(firewall, 'geoip'): + set_name = f'GEOIP_CC_{path[1]}_{path[3]}' + if path[0] == 'name': + out['name'].append(set_name) + elif path[0] == 'ipv6_name': + out['ipv6_name'].append(set_name) + updated = True + + if 'delete' in node_diff: + for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): + set_name = f'GEOIP_CC_{path[1]}_{path[3]}' + if path[0] == 'name': + out['deleted_name'].append(set_name) + elif path[0] == 'ipv6-name': + out['deleted_ipv6_name'].append(set_name) + updated = True + + if updated: + return out + + return False + def get_config(config=None): if config: conf = config @@ -162,6 +220,8 @@ def get_config(config=None): key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + firewall['geoip_updated'] = geoip_updated(conf, firewall) + return firewall def verify_rule(firewall, rule_conf, ipv6): @@ -207,6 +267,16 @@ def verify_rule(firewall, rule_conf, ipv6): if side in rule_conf: side_conf = rule_conf[side] + if dict_search_args(side_conf, 'geoip', 'country_code'): + if 'address' in side_conf: + raise ConfigError('Address and GeoIP cannot both be defined') + + if dict_search_args(side_conf, 'group', 'address_group'): + raise ConfigError('Address-group and GeoIP cannot both be defined') + + if dict_search_args(side_conf, 'group', 'network_group'): + raise ConfigError('Network-group and GeoIP cannot both be defined') + if 'group' in side_conf: if {'address_group', 'network_group'} <= set(side_conf['group']): raise ConfigError('Only one address-group or network-group can be specified') @@ -235,11 +305,34 @@ def verify_rule(firewall, rule_conf, ipv6): if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') +def verify_nested_group(group_name, group, groups, seen): + if 'include' not in group: + return + + for g in group['include']: + if g not in groups: + raise ConfigError(f'Nested group "{g}" does not exist') + + if g in seen: + raise ConfigError(f'Group "{group_name}" has a circular reference') + + seen.append(g) + + if 'include' in groups[g]: + verify_nested_group(g, groups[g], groups, seen) + def verify(firewall): if 'config_trap' in firewall and firewall['config_trap'] == 'enable': if not firewall['trap_targets']: raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') + if 'group' in firewall: + for group_type in nested_group_types: + if group_type in firewall['group']: + groups = firewall['group'][group_type] + for group_name, group in groups.items(): + verify_nested_group(group_name, group, groups, []) + for name in ['name', 'ipv6_name']: if name in firewall: for name_id, name_conf in firewall[name].items(): @@ -271,55 +364,75 @@ def verify(firewall): return None -def cleanup_rule(table, jump_chain): - commands = [] - chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains - for chain in chains: - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - for line in results: - if f'jump {jump_chain}' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') - return commands - def cleanup_commands(firewall): commands = [] - commands_end = [] + commands_chains = [] + commands_sets = [] for table in ['ip filter', 'ip6 filter']: + name_node = 'name' if table == 'ip filter' else 'ipv6_name' + chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6' - json_str = cmd(f'nft -j list table {table}') + iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains + + geoip_list = [] + if firewall['geoip_updated']: + geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' + geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] + + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) + if 'nftables' not in obj: continue + for item in obj['nftables']: if 'chain' in item: chain = item['chain']['name'] - if chain in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']: - if 'state_policy' not in firewall: - commands.append(f'delete chain {table} {chain}') - else: - commands.append(f'flush chain {table} {chain}') - elif chain not in preserve_chains and not chain.startswith("VZONE"): - if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - else: - commands += cleanup_rule(table, chain) - commands.append(f'delete chain {table} {chain}') - elif 'rule' in item: + if chain in preserve_chains or chain.startswith("VZONE"): + continue + + if chain == state_chain: + command = 'delete' if 'state_policy' not in firewall else 'flush' + commands_chains.append(f'{command} chain {table} {chain}') + elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + else: + commands_chains.append(f'delete chain {table} {chain}') + + if 'rule' in item: rule = item['rule'] - if rule['chain'] in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: - if 'expr' in rule and any([True for expr in rule['expr'] if dict_search_args(expr, 'jump', 'target') == state_chain]): - if 'state_policy' not in firewall: - chain = rule['chain'] - handle = rule['handle'] + chain = rule['chain'] + handle = rule['handle'] + + if chain in iface_chains: + target, _ = next(dict_search_recursive(rule['expr'], 'target')) + + if target == state_chain and 'state_policy' not in firewall: + commands.append(f'delete rule {table} {chain} handle {handle}') + + if target.startswith(chain_prefix): + if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None: commands.append(f'delete rule {table} {chain} handle {handle}') - elif 'set' in item: + + if 'set' in item: set_name = item['set']['name'] - commands_end.append(f'delete set {table} {set_name}') - return commands + commands_end + + if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: + commands_sets.append(f'delete set {table} {set_name}') + continue + + if set_name.startswith("RECENT_"): + commands_sets.append(f'delete set {table} {set_name}') + continue + + for prefix, group_type in group_set_prefix.items(): + if set_name.startswith(prefix): + group_name = set_name.replace(prefix, "", 1) + if dict_search_args(firewall, 'group', group_type, group_name) != None: + commands_sets.append(f'flush set {table} {set_name}') + else: + commands_sets.append(f'delete set {table} {set_name}') + return commands + commands_chains + commands_sets def generate(firewall): if not os.path.exists(nftables_conf): @@ -328,7 +441,6 @@ def generate(firewall): firewall['cleanup_commands'] = cleanup_commands(firewall) render(nftables_conf, 'firewall/nftables.j2', firewall) - render(nftables_defines_conf, 'firewall/nftables-defines.j2', firewall) return None def apply_sysfs(firewall): @@ -408,6 +520,27 @@ def apply(firewall): if install_result == 1: raise ConfigError('Failed to apply firewall') + # set fireall group domain-group xxx + if 'group' in firewall: + if 'domain_group' in firewall['group']: + # T970 Enable a resolver (systemd daemon) that checks + # domain-group addresses and update entries for domains by timeout + # If router loaded without internet connection or for synchronization + call('systemctl restart vyos-domain-group-resolve.service') + for group, group_config in firewall['group']['domain_group'].items(): + domains = [] + if group_config.get('address') is not None: + for address in group_config.get('address'): + domains.append(address) + # Add elements to domain-group, try to resolve domain => ip + # and add elements to nft set + ip_dict = get_ips_domains_dict(domains) + elements = sum(ip_dict.values(), []) + nft_init_set(f'D_{group}') + nft_add_set_elements(f'D_{group}', elements) + else: + call('systemctl stop vyos-domain-group-resolve.service') + if 'state_policy' in firewall and not state_policy_rule_exists(): for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']: cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') @@ -420,6 +553,12 @@ def apply(firewall): if firewall['policy_resync']: resync_policy_route() + if firewall['geoip_updated']: + # Call helper script to Update set contents + if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: + print('Updating GeoIP. Please wait...') + geoip_update(firewall) + post_apply_trap(firewall) return None diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 7f7a98b04..7750c1247 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -22,6 +22,7 @@ import ipaddress from ipaddress import ip_address +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.ifconfig import Section @@ -109,6 +110,9 @@ def _nftables_config(configured_ifaces, direction, length=None): iface_prefix = "o" if direction == "egress" else "i" rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"' nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}') + # Also add IPv6 ingres logging + if nftables_table == nftables_nflog_table: + nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}') # change nftables for command in nftable_commands: @@ -172,7 +176,7 @@ def verify(flow_config): if interface not in Section.interfaces(): # Changed from error to warning to allow adding dynamic interfaces # and interface templates - print(f'Warning: Interface "{interface}" is not presented in the system') + Warning(f'Interface "{interface}" is not presented in the system') # check sFlow configuration if 'sflow' in flow_config: @@ -200,7 +204,13 @@ def verify(flow_config): if 'agent_address' in flow_config['sflow']: tmp = flow_config['sflow']['agent_address'] if not is_addr_assigned(tmp): - print(f'Warning: Configured "sflow agent-address {tmp}" does not exist in the system!') + raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!') + + # Check if configured netflow source-address exist in the system + if 'source_address' in flow_config['sflow']: + if not is_addr_assigned(flow_config['sflow']['source_address']): + tmp = flow_config['sflow']['source_address'] + raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!') # check NetFlow configuration if 'netflow' in flow_config: @@ -212,7 +222,7 @@ def verify(flow_config): if 'source_address' in flow_config['netflow']: if not is_addr_assigned(flow_config['netflow']['source_address']): tmp = flow_config['netflow']['source_address'] - print(f'Warning: Configured "netflow source-address {tmp}" does not exist on the system!') + raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!') # Check if engine-id compatible with selected protocol version if 'engine_id' in flow_config['netflow']: diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index f939f9469..e14050dd3 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -28,7 +28,6 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call -from vyos.util import is_systemd_service_running from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -161,12 +160,7 @@ def apply(ha): call(f'systemctl stop {service_name}') return None - # XXX: T3944 - reload keepalived configuration if service is already running - # to not cause any service disruption when applying changes. - if is_systemd_service_running(service_name): - call(f'systemctl reload {service_name}') - else: - call(f'systemctl restart {service_name}') + call(f'systemctl reload-or-restart {service_name}') return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 4167594e3..82ce1b41a 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -109,20 +109,22 @@ def get_config(config=None): for interface, interface_config in bond['member']['interface'].items(): # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: interface_config.update({'is_bridge_member' : tmp}) + if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') - if tmp and bond['ifname'] not in tmp: - interface_config.update({'is_bond_member' : tmp}) + for tmp in is_member(conf, interface, 'bonding'): + if bond['ifname'] == tmp: + continue + bond['member']['interface'][interface].update({'is_bond_member' : tmp}) # Check if member interface is used as source-interface on another interface tmp = is_source_interface(conf, interface) - if tmp: interface_config.update({'is_source_interface' : tmp}) + if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp}) # bond members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: interface_config.update({'has_address' : ''}) + if tmp: bond['member']['interface'][interface].update({'has_address' : {}}) return bond @@ -167,11 +169,11 @@ def verify(bond): raise ConfigError(error_msg + 'it does not exist!') if 'is_bridge_member' in interface_config: - tmp = next(iter(interface_config['is_bridge_member'])) + tmp = interface_config['is_bridge_member'] raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: - tmp = next(iter(interface_config['is_bond_member'])) + tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 38ae727c1..9f8eb31c6 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -118,11 +118,11 @@ def verify(bridge): raise ConfigError('Loopback interface "lo" can not be added to a bridge') if 'is_bridge_member' in interface_config: - tmp = next(iter(interface_config['is_bridge_member'])) + tmp = interface_config['is_bridge_member'] raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: - tmp = next(iter(interface_config['is_bond_member'])) + tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: @@ -134,7 +134,7 @@ def verify(bridge): if 'enable_vlan' in bridge: if 'has_vlan' in interface_config: - raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!') if 'wlan' in interface: raise ConfigError(error_msg + 'VLAN aware cannot be set!') diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4750ca3e8..280a62b9a 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -39,6 +39,8 @@ from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import VTunIf from vyos.pki import load_dh_parameters from vyos.pki import load_private_key +from vyos.pki import sort_ca_chain +from vyos.pki import verify_ca_chain from vyos.pki import wrap_certificate from vyos.pki import wrap_crl from vyos.pki import wrap_dh_parameters @@ -148,8 +150,14 @@ def verify_pki(openvpn): if 'ca_certificate' not in tls: raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') - if tls['ca_certificate'] not in pki['ca']: - raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + for ca_name in tls['ca_certificate']: + if ca_name not in pki['ca']: + raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + + if len(tls['ca_certificate']) > 1: + sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) + if not verify_ca_chain(sorted_chain, pki['ca']): + raise ConfigError(f'CA certificates are not a valid chain') if mode != 'client' and 'auth_key' not in tls: if 'certificate' not in tls: @@ -516,21 +524,28 @@ def generate_pki_files(openvpn): if tls: if 'ca_certificate' in tls: - cert_name = tls['ca_certificate'] - pki_ca = pki['ca'][cert_name] + cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') + crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - if 'certificate' in pki_ca: - cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') - write_file(cert_path, wrap_certificate(pki_ca['certificate']), - user=user, group=group, mode=0o600) + if os.path.exists(cert_path): + os.unlink(cert_path) + + if os.path.exists(crl_path): + os.unlink(crl_path) + + for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): + pki_ca = pki['ca'][cert_name] + + if 'certificate' in pki_ca: + write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", + user=user, group=group, mode=0o600, append=True) - if 'crl' in pki_ca: - for crl in pki_ca['crl']: - crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - write_file(crl_path, wrap_crl(crl), user=user, group=group, - mode=0o600) + if 'crl' in pki_ca: + for crl in pki_ca['crl']: + write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, + mode=0o600, append=True) - openvpn['tls']['crl'] = True + openvpn['tls']['crl'] = True if 'certificate' in tls: cert_name = tls['certificate'] diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 0d6ec9ace..5490a794d 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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,9 +18,11 @@ import os from vyos.config import Config from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.configverify import verify_interface_exists from vyos.util import call +from vyos.util import get_interface_config from vyos.template import render +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -49,6 +51,20 @@ def verify(ntp): raise ConfigError('NTP server not configured') verify_vrf(ntp) + + if 'interface' in ntp: + # If ntpd should listen on a given interface, ensure it exists + for interface in ntp['interface']: + verify_interface_exists(interface) + + # If we run in a VRF, our interface must belong to this VRF, too + if 'vrf' in ntp: + tmp = get_interface_config(interface) + vrf_name = ntp['vrf'] + if 'master' not in tmp or tmp['master'] != vrf_name: + raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ + f'does not belong to this VRF!') + return None def generate(ntp): diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index efa3578b4..29ed7b1b7 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -29,12 +29,60 @@ from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters from vyos.util import ask_input +from vyos.util import call +from vyos.util import dict_search_args from vyos.util import dict_search_recursive from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() +# keys to recursively search for under specified path, script to call if update required +sync_search = [ + { + 'keys': ['certificate'], + 'path': ['service', 'https'], + 'script': '/usr/libexec/vyos/conf_mode/https.py' + }, + { + 'keys': ['certificate', 'ca_certificate'], + 'path': ['interfaces', 'ethernet'], + 'script': '/usr/libexec/vyos/conf_mode/interfaces-ethernet.py' + }, + { + 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'], + 'path': ['interfaces', 'openvpn'], + 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py' + }, + { + 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], + 'path': ['vpn', 'ipsec'], + 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py' + }, + { + 'keys': ['certificate', 'ca_certificate'], + 'path': ['vpn', 'openconnect'], + 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py' + }, + { + 'keys': ['certificate', 'ca_certificate'], + 'path': ['vpn', 'sstp'], + 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py' + } +] + +# key from other config nodes -> key in pki['changed'] and pki +sync_translate = { + 'certificate': 'certificate', + 'ca_certificate': 'ca', + 'dh_params': 'dh', + 'local_key': 'key_pair', + 'remote_key': 'key_pair', + 'shared_secret_key': 'openvpn', + 'auth_key': 'openvpn', + 'crypt_key': 'openvpn' +} + def get_config(config=None): if config: conf = config @@ -47,12 +95,21 @@ def get_config(config=None): no_tag_node_value_mangle=True) pki['changed'] = {} - tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_')) + tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True) if tmp: pki['changed'].update({'ca' : tmp}) - tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_')) + tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True) if tmp: pki['changed'].update({'certificate' : tmp}) + tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True) + if tmp: pki['changed'].update({'dh' : tmp}) + + tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True) + if tmp: pki['changed'].update({'key_pair' : tmp}) + + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True) + if tmp: pki['changed'].update({'openvpn' : tmp}) + # We only merge on the defaults of there is a configuration at all if conf.exists(base): default_values = defaults(base) @@ -164,17 +221,30 @@ def verify(pki): if 'changed' in pki: # if the list is getting longer, we can move to a dict() and also embed the # search key as value from line 173 or 176 - for cert_type in ['ca', 'certificate']: - if not cert_type in pki['changed']: - continue - for certificate in pki['changed'][cert_type]: - if cert_type not in pki or certificate not in pki['changed'][cert_type]: - if cert_type == 'ca': - if certificate in dict_search_recursive(pki['system'], 'ca_certificate'): - raise ConfigError(f'CA certificate "{certificate}" is still in use!') - elif cert_type == 'certificate': - if certificate in dict_search_recursive(pki['system'], 'certificate'): - raise ConfigError(f'Certificate "{certificate}" is still in use!') + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + + if changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if not node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + + if not search_dict: + continue + + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path_str = " ".join(search['path'] + found_path) + raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"') return None @@ -188,7 +258,38 @@ def apply(pki): if not pki: return None - # XXX: restart services if the content of a certificate changes + if 'changed' in pki: + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + + if changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + + if not search_dict: + continue + + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path_str = ' '.join(search['path'] + found_path) + print(f'pki: Updating config: {path_str} {found_name}') + + script = search['script'] + if found_path[0] == 'interfaces': + ifname = found_path[2] + call(f'VYOS_TAGNODE_VALUE={ifname} {script}') + else: + call(script) return None diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py index 1108aebe6..58c5fd93d 100755 --- a/src/conf_mode/policy-route-interface.py +++ b/src/conf_mode/policy-route-interface.py @@ -24,6 +24,7 @@ from vyos.config import Config from vyos.ifconfig import Section from vyos.template import render from vyos.util import cmd +from vyos.util import run from vyos import ConfigError from vyos import airbag airbag.enable() @@ -47,6 +48,11 @@ def get_config(config=None): return if_policy +def verify_chain(table, chain): + # Verify policy route applied + code = run(f'nft list chain {table} {chain}') + return code == 0 + def verify(if_policy): # bail out early - looks like removal from running config if not if_policy: @@ -62,6 +68,12 @@ def verify(if_policy): if route_name not in if_policy['policy'][route]: raise ConfigError(f'Invalid policy route name "{name}"') + nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_' + nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle' + + if not verify_chain(nft_table, nft_prefix + route_name): + raise ConfigError('Policy route did not apply') + return None def generate(if_policy): diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 5de341beb..9fddbd2c6 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -25,6 +25,7 @@ from vyos.config import Config from vyos.template import render from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive from vyos.util import run from vyos import ConfigError from vyos import airbag @@ -33,6 +34,9 @@ airbag.enable() mark_offset = 0x7FFFFFFF nftables_conf = '/run/nftables_policy.conf' +ROUTE_PREFIX = 'VYOS_PBR_' +ROUTE6_PREFIX = 'VYOS_PBR6_' + preserve_chains = [ 'VYOS_PBR_PREROUTING', 'VYOS_PBR_POSTROUTING', @@ -46,6 +50,16 @@ valid_groups = [ 'port_group' ] +group_set_prefix = { + 'A_': 'address_group', + 'A6_': 'ipv6_address_group', +# 'D_': 'domain_group', + 'M_': 'mac_group', + 'N_': 'network_group', + 'N6_': 'ipv6_network_group', + 'P_': 'port_group' +} + def get_policy_interfaces(conf): out = {} interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, @@ -166,37 +180,55 @@ def verify(policy): return None -def cleanup_rule(table, jump_chain): - commands = [] - results = cmd(f'nft -a list table {table}').split("\n") - for line in results: - if f'jump {jump_chain}' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') - return commands - def cleanup_commands(policy): commands = [] + commands_chains = [] + commands_sets = [] for table in ['ip mangle', 'ip6 mangle']: - json_str = cmd(f'nft -j list table {table}') + route_node = 'route' if table == 'ip mangle' else 'route6' + chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX + + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) if 'nftables' not in obj: continue for item in obj['nftables']: if 'chain' in item: chain = item['chain']['name'] - if not chain.startswith("VYOS_PBR"): + if chain in preserve_chains or not chain.startswith("VYOS_PBR"): continue + + if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + else: + commands_chains.append(f'delete chain {table} {chain}') + + if 'rule' in item: + rule = item['rule'] + chain = rule['chain'] + handle = rule['handle'] + if chain not in preserve_chains: - if table == 'ip mangle' and dict_search_args(policy, 'route', chain.replace("VYOS_PBR_", "", 1)): - commands.append(f'flush chain {table} {chain}') - elif table == 'ip6 mangle' and dict_search_args(policy, 'route6', chain.replace("VYOS_PBR6_", "", 1)): - commands.append(f'flush chain {table} {chain}') - else: - commands += cleanup_rule(table, chain) - commands.append(f'delete chain {table} {chain}') - return commands + continue + + target, _ = next(dict_search_recursive(rule['expr'], 'target')) + + if target.startswith(chain_prefix): + if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: + commands.append(f'delete rule {table} {chain} handle {handle}') + + if 'set' in item: + set_name = item['set']['name'] + + for prefix, group_type in group_set_prefix.items(): + if set_name.startswith(prefix): + group_name = set_name.replace(prefix, "", 1) + if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: + commands_sets.append(f'flush set {table} {set_name}') + else: + commands_sets.append(f'delete set {table} {set_name}') + + return commands + commands_chains + commands_sets def generate(policy): if not os.path.exists(nftables_conf): diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index ef6008140..3008a20e0 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -150,6 +150,16 @@ def verify(policy): tmp = dict_search('match.ipv6.address.prefix_list', rule_config) if tmp and tmp not in policy.get('prefix_list6', []): raise ConfigError(f'prefix-list6 {tmp} does not exist!') + + # Specified access_list6 in nexthop must exist + tmp = dict_search('match.ipv6.nexthop.access_list', rule_config) + if tmp and tmp not in policy.get('access_list6', []): + raise ConfigError(f'access_list6 {tmp} does not exist!') + + # Specified prefix-list6 in nexthop must exist + tmp = dict_search('match.ipv6.nexthop.prefix_list', rule_config) + if tmp and tmp not in policy.get('prefix_list6', []): + raise ConfigError(f'prefix-list6 {tmp} does not exist!') # When routing protocols are active some use prefix-lists, route-maps etc. # to apply the systems routing policy to the learned or redistributed routes. diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index a9173ab87..01f14df61 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,6 +19,7 @@ import os from sys import exit from sys import argv +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_prefix_list @@ -169,6 +170,16 @@ def verify(bgp): peer_group = peer_config['peer_group'] if 'remote_as' in peer_config and 'remote_as' in bgp['peer_group'][peer_group]: raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') + if 'interface' in peer_config: + if 'peer_group' in peer_config['interface']: + peer_group = peer_config['interface']['peer_group'] + if 'remote_as' in peer_config['interface'] and 'remote_as' in bgp['peer_group'][peer_group]: + raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') + if 'v6only' in peer_config['interface']: + if 'peer_group' in peer_config['interface']['v6only']: + peer_group = peer_config['interface']['v6only']['peer_group'] + if 'remote_as' in peer_config['interface']['v6only'] and 'remote_as' in bgp['peer_group'][peer_group]: + raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!') # Only checks for ipv4 and ipv6 neighbors # Check if neighbor address is assigned as system interface address @@ -188,6 +199,9 @@ def verify(bgp): if 'source_interface' in peer_config['interface']: raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') + if 'address_family' not in peer_config: + Warning(f'BGP neighbor "{peer}" requires address-family!') + for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec', 'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec', 'l2vpn_evpn']: diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py new file mode 100755 index 000000000..c1a1a45e1 --- /dev/null +++ b/src/conf_mode/protocols_eigrp.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +from sys import exit +from sys import argv + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + vrf = None + if len(argv) > 1: + vrf = argv[1] + + base_path = ['protocols', 'eigrp'] + + # eqivalent of the C foo ? 'a' : 'b' statement + base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path + eigrp = conf.get_config_dict(base, key_mangling=('-', '_'), + 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 + # instead of the VRF instance. + if vrf: eigrp.update({'vrf' : vrf}) + + if not conf.exists(base): + eigrp.update({'deleted' : ''}) + if not vrf: + # We are running in the default VRF context, thus we can not delete + # our main EIGRP instance if there are dependent EIGRP VRF instances. + eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + return eigrp + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify(). + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = conf.get_config_dict(['policy']) + # Merge policy dict into "regular" config dict + eigrp = dict_merge(tmp, eigrp) + + import pprint + pprint.pprint(eigrp) + return eigrp + +def verify(eigrp): + pass + +def generate(eigrp): + if not eigrp or 'deleted' in eigrp: + return None + + eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2 + eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp) + eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp) + +def apply(eigrp): + eigrp_daemon = 'eigrpd' + 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(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in eigrp: + frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + + # Generate empty helper string which can be ammended to FRR commands, it + # will be either empty (default VRF) or contain the "vrf <name" statement + vrf = '' + if 'vrf' in eigrp: + vrf = ' vrf ' + eigrp['vrf'] + + frr_cfg.load_configuration(eigrp_daemon) + frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) + if 'frr_eigrpd_config' in eigrp: + frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config']) + frr_cfg.commit_configuration(eigrp_daemon) + + return None + +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/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 92b335085..b247ce2ab 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -104,8 +104,8 @@ def apply(nhrp): if rule_handle: remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle) - action = 'reload-or-restart' if nhrp and 'tunnel' in nhrp else 'stop' - run(f'systemctl {action} opennhrp') + action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' + run(f'systemctl {action} opennhrp.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index a76c1ce76..c78d90396 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py new file mode 100755 index 000000000..5440d1056 --- /dev/null +++ b/src/conf_mode/service_event_handler.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 json +from pathlib import Path + +from vyos.config import Config +from vyos.util import call, dict_search +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + +service_name = 'vyos-event-handler' +service_conf = Path(f'/run/{service_name}.conf') + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['service', 'event-handler', 'event'] + config = conf.get_config_dict(base, + get_first_key=True, + no_tag_node_value_mangle=True) + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if not config: + return None + + for name, event_config in config.items(): + if not dict_search('filter.pattern', event_config) or not dict_search( + 'script.path', event_config): + raise ConfigError( + 'Event-handler: both pattern and script path items are mandatory' + ) + + if dict_search('script.environment.message', event_config): + raise ConfigError( + 'Event-handler: "message" environment variable is reserved for log message text' + ) + + +def generate(config): + if not config: + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + # Write configuration file + conf_json = json.dumps(config, indent=4) + service_conf.write_text(conf_json) + + return None + + +def apply(config): + if config: + call(f'systemctl restart {service_name}.service') + else: + call(f'systemctl stop {service_name}.service') + + +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_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 559d1bcd5..61f484129 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -53,6 +53,8 @@ default_config_data = { 'radius_nas_ip': '', 'radius_source_address': '', 'radius_shaper_attr': '', + 'radius_shaper_enable': False, + 'radius_shaper_multiplier': '', 'radius_shaper_vendor': '', 'radius_dynamic_author': '', 'thread_cnt': get_half_cpus() @@ -196,6 +198,18 @@ def get_config(config=None): if conf.exists(['nas-ip-address']): ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + if conf.exists(['rate-limit', 'attribute']): + ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + + if conf.exists(['rate-limit', 'enable']): + ipoe['radius_shaper_enable'] = True + + if conf.exists(['rate-limit', 'multiplier']): + ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + + if conf.exists(['rate-limit', 'vendor']): + ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) + if conf.exists(['source-address']): ipoe['radius_source_address'] = conf.return_value(['source-address']) diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 102a87318..62f5e1ddf 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -99,15 +99,31 @@ def get_config(config=None): monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) monitoring['nft_chains'] = get_nft_filter_chains() - if 'authentication' in monitoring or \ - 'url' in monitoring: - monitoring['influxdb_configured'] = True + # Redefine azure group-metrics 'single-table' and 'table-per-metric' + if 'azure_data_explorer' in monitoring: + if 'single-table' in monitoring['azure_data_explorer']['group_metrics']: + monitoring['azure_data_explorer']['group_metrics'] = 'SingleTable' + else: + monitoring['azure_data_explorer']['group_metrics'] = 'TablePerMetric' + # Set azure env + if 'authentication' in monitoring['azure_data_explorer']: + auth_config = monitoring['azure_data_explorer']['authentication'] + if {'client_id', 'client_secret', 'tenant_id'} <= set(auth_config): + os.environ['AZURE_CLIENT_ID'] = auth_config['client_id'] + os.environ['AZURE_CLIENT_SECRET'] = auth_config['client_secret'] + os.environ['AZURE_TENANT_ID'] = auth_config['tenant_id'] # Ignore default XML values if config doesn't exists # Delete key from dict + if not conf.exists(base + ['influxdb']): + del monitoring['influxdb'] + if not conf.exists(base + ['prometheus-client']): del monitoring['prometheus_client'] + if not conf.exists(base + ['azure-data-explorer']): + del monitoring['azure_data_explorer'] + return monitoring def verify(monitoring): @@ -115,15 +131,34 @@ def verify(monitoring): if not monitoring: return None - if 'influxdb_configured' in monitoring: - if 'authentication' not in monitoring or \ - 'organization' not in monitoring['authentication'] or \ - 'token' not in monitoring['authentication']: - raise ConfigError(f'Authentication "organization and token" are mandatory!') + # Verify influxdb + if 'influxdb' in monitoring: + if 'authentication' not in monitoring['influxdb'] or \ + 'organization' not in monitoring['influxdb']['authentication'] or \ + 'token' not in monitoring['influxdb']['authentication']: + raise ConfigError(f'influxdb authentication "organization and token" are mandatory!') + + if 'url' not in monitoring['influxdb']: + raise ConfigError(f'Monitoring influxdb "url" is mandatory!') - if 'url' not in monitoring: + # Verify azure-data-explorer + if 'azure_data_explorer' in monitoring: + if 'authentication' not in monitoring['azure_data_explorer'] or \ + 'client_id' not in monitoring['azure_data_explorer']['authentication'] or \ + 'client_secret' not in monitoring['azure_data_explorer']['authentication'] or \ + 'tenant_id' not in monitoring['azure_data_explorer']['authentication']: + raise ConfigError(f'Authentication "client-id, client-secret and tenant-id" are mandatory!') + + if 'database' not in monitoring['azure_data_explorer']: + raise ConfigError(f'Monitoring "database" is mandatory!') + + if 'url' not in monitoring['azure_data_explorer']: raise ConfigError(f'Monitoring "url" is mandatory!') + if monitoring['azure_data_explorer']['group_metrics'] == 'SingleTable' and \ + 'table' not in monitoring['azure_data_explorer']: + raise ConfigError(f'Monitoring "table" name for single-table mode is mandatory!') + # Verify Splunk if 'splunk' in monitoring: if 'authentication' not in monitoring['splunk'] or \ diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 71b758399..ff7caaa84 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -17,7 +17,7 @@ import os from sys import exit - +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -79,22 +79,35 @@ def verify(rtradv): if 'interface' not in rtradv: return None - for interface in rtradv['interface']: - interface = rtradv['interface'][interface] + for interface, interface_config in rtradv['interface'].items(): if 'prefix' in interface: - for prefix in interface['prefix']: - prefix = interface['prefix'][prefix] - valid_lifetime = prefix['valid_lifetime'] + for prefix, prefix_config in interface_config['prefix'].items(): + valid_lifetime = prefix_config['valid_lifetime'] if valid_lifetime == 'infinity': valid_lifetime = 4294967295 - preferred_lifetime = prefix['preferred_lifetime'] + preferred_lifetime = prefix_config['preferred_lifetime'] if preferred_lifetime == 'infinity': preferred_lifetime = 4294967295 if not (int(valid_lifetime) > int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if 'name_server_lifetime' in interface_config: + # man page states: + # The maximum duration how long the RDNSS entries are used for name + # resolution. A value of 0 means the nameserver must no longer be + # used. The value, if not 0, must be at least MaxRtrAdvInterval. To + # ensure stale RDNSS info gets removed in a timely fashion, this + # should not be greater than 2*MaxRtrAdvInterval. + lifetime = int(interface_config['name_server_lifetime']) + interval_max = int(interface_config['interval']['max']) + if lifetime > 0: + if lifetime < int(interval_max): + raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!') + if lifetime > 2* interval_max: + Warning(f'RDNSS lifetime should not exceed "{2 * interval_max}" which is two times "interval max"!') + return None def generate(rtradv): @@ -105,15 +118,16 @@ def generate(rtradv): return None def apply(rtradv): + systemd_service = 'radvd.service' if not rtradv: # bail out early - looks like removal from running config - call('systemctl stop radvd.service') + call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart radvd.service') + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py new file mode 100755 index 000000000..e7c3ca59c --- /dev/null +++ b/src/conf_mode/service_sla.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +owamp_config_dir = '/etc/owamp-server' +owamp_config_file = f'{owamp_config_dir}/owamp-server.conf' +systemd_override_owamp = r'/etc/systemd/system/owamp-server.d/20-override.conf' + +twamp_config_dir = '/etc/twamp-server' +twamp_config_file = f'{twamp_config_dir}/twamp-server.conf' +systemd_override_twamp = r'/etc/systemd/system/twamp-server.d/20-override.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'sla'] + if not conf.exists(base): + return None + + sla = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + sla = dict_merge(default_values, sla) + + # Ignore default XML values if config doesn't exists + # Delete key from dict + if not conf.exists(base + ['owamp-server']): + del sla['owamp_server'] + if not conf.exists(base + ['twamp-server']): + del sla['twamp_server'] + + return sla + +def verify(sla): + if not sla: + return None + +def generate(sla): + if not sla: + return None + + render(owamp_config_file, 'sla/owamp-server.conf.j2', sla) + render(systemd_override_owamp, 'sla/owamp-override.conf.j2', sla) + + render(twamp_config_file, 'sla/twamp-server.conf.j2', sla) + render(systemd_override_twamp, 'sla/twamp-override.conf.j2', sla) + + return None + +def apply(sla): + owamp_service = 'owamp-server.service' + twamp_service = 'twamp-server.service' + + call('systemctl daemon-reload') + + if not sla or 'owamp_server' not in sla: + call(f'systemctl stop {owamp_service}') + + if os.path.exists(owamp_config_file): + os.unlink(owamp_config_file) + + if not sla or 'twamp_server' not in sla: + call(f'systemctl stop {twamp_service}') + if os.path.exists(twamp_config_file): + os.unlink(twamp_config_file) + + if sla and 'owamp_server' in sla: + call(f'systemctl reload-or-restart {owamp_service}') + + if sla and 'twamp_server' in sla: + call(f'systemctl reload-or-restart {twamp_service}') + + return None + +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/snmp.py b/src/conf_mode/snmp.py index ae060580d..5cd24db32 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -293,7 +293,15 @@ def apply(snmp): call(f'systemctl restart {systemd_service}') # Enable AgentX in FRR - call('vtysh -c "configure terminal" -c "agentx" >/dev/null') + # This should be done for each daemon individually because common command + # works only if all the daemons started with SNMP support + frr_daemons_list = [ + 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra' + ] + for frr_daemon in frr_daemons_list: + call( + f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null' + ) return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 487e8c229..28669694b 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -33,6 +33,9 @@ airbag.enable() config_file = r'/run/sshd/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' +sshguard_config_file = '/etc/sshguard/sshguard.conf' +sshguard_whitelist = '/etc/sshguard/whitelist' + key_rsa = '/etc/ssh/ssh_host_rsa_key' key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' @@ -54,6 +57,11 @@ def get_config(config=None): # pass config file path - used in override template ssh['config_file'] = config_file + # Ignore default XML values if config doesn't exists + # Delete key from dict + if not conf.exists(base + ['dynamic-protection']): + del ssh['dynamic_protection'] + return ssh def verify(ssh): @@ -86,6 +94,10 @@ def generate(ssh): render(config_file, 'ssh/sshd_config.j2', ssh) render(systemd_override, 'ssh/override.conf.j2', ssh) + + if 'dynamic_protection' in ssh: + render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh) + render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh) # Reload systemd manager configuration call('systemctl daemon-reload') @@ -95,7 +107,12 @@ def apply(ssh): if not ssh: # SSH access is removed in the commit call('systemctl stop ssh.service') + call('systemctl stop sshguard.service') return None + if 'dynamic_protection' not in ssh: + call('systemctl stop sshguard.service') + else: + call('systemctl restart sshguard.service') call('systemctl restart ssh.service') return None diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 05fc3a97a..0c5063ed3 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -64,6 +64,11 @@ def apply(opt): value = '0' if (tmp != None) else '1' write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) + # enable/disable IPv4 directed broadcast forwarding + tmp = dict_search('disable_directed_broadcast', opt) + value = '0' if (tmp != None) else '1' + write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value) + # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index a9d3bbe31..20132456c 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -52,8 +52,6 @@ def get_config(config=None): { 'global': { 'log-file': '/var/log/messages', - 'max-size': 262144, - 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', 'selectors': '*.notice;local7.debug', 'max-files': '5', 'preserver_fqdn': False diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py new file mode 100755 index 000000000..1af0055f6 --- /dev/null +++ b/src/conf_mode/system_frr.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +from pathlib import Path +from sys import exit + +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.logger import syslog +from vyos.template import render_to_string +from vyos.util import read_file, write_file, run +airbag.enable() + +# path to daemons config and config status files +config_file = '/etc/frr/daemons' +vyos_status_file = '/tmp/vyos-config-status' +# path to watchfrr for FRR control +watchfrr = '/usr/lib/frr/watchfrr.sh' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['system', 'frr'] + frr_config = conf.get_config_dict(base, get_first_key=True) + + return frr_config + + +def verify(frr_config): + # Nothing to verify here + pass + + +def generate(frr_config): + # read daemons config file + daemons_config_current = read_file(config_file) + # generate new config file + daemons_config_new = render_to_string('frr/daemons.frr.tmpl', frr_config) + # update configuration file if this is necessary + if daemons_config_new != daemons_config_current: + syslog.warning('FRR daemons configuration file need to be changed') + write_file(config_file, daemons_config_new) + frr_config['config_file_changed'] = True + + +def apply(frr_config): + # check if this is initial commit during boot or intiated by CLI + # if the file exists, this must be CLI commit + commit_type_cli = Path(vyos_status_file).exists() + # display warning to user + if commit_type_cli and frr_config.get('config_file_changed'): + # Since FRR restart is not safe thing, better to give + # control over this to users + print(''' + You need to reboot a router (preferred) or restart FRR + to apply changes in modules settings + ''') + # restart FRR automatically. DUring the initial boot this should be + # safe in most cases + if not commit_type_cli and frr_config.get('config_file_changed'): + syslog.warning('Restarting FRR to apply changes in modules') + run(f'{watchfrr} restart') + + +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/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index db53463cf..23e5162ba 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -20,6 +20,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict +from vyos.configdict import dict_merge from vyos.configverify import verify_accel_ppp_base_service from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key @@ -50,10 +51,10 @@ def get_config(config=None): # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) - if sstp: sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + get_first_key=True, + no_tag_node_value_mangle=True) return sstp @@ -121,7 +122,6 @@ def generate(sstp): ca_cert_name = sstp['ssl']['ca_certificate'] pki_ca = sstp['pki']['ca'][ca_cert_name] - write_file(cert_file_path, wrap_certificate(pki_cert['certificate'])) write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate'])) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index f2d041083..1b4156895 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -83,7 +83,8 @@ def get_config(config=None): conf = Config() base = ['vrf'] - vrf = conf.get_config_dict(base, get_first_key=True) + vrf = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, get_first_key=True) # determine which VRF has been removed for name in node_changed(conf, base + ['name']): @@ -112,8 +113,14 @@ def verify(vrf): f'static routes installed!') if 'name' in vrf: + reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type", + "vrf"] table_ids = [] for name, config in vrf['name'].items(): + # Reserved VRF names + if name in reserved_names: + raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') + # table id is mandatory if 'table' not in config: raise ConfigError(f'VRF "{name}" table id is mandatory!') @@ -152,7 +159,7 @@ def apply(vrf): # set the default VRF global behaviour bind_all = '0' - if 'bind-to-all' in vrf: + if 'bind_to_all' in vrf: bind_all = '1' sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all) sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all) @@ -222,6 +229,15 @@ def apply(vrf): # add VRF description if available vrf_if.set_alias(config.get('description', '')) + # Enable/Disable IPv4 forwarding + tmp = dict_search('ip.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + vrf_if.set_ipv4_forwarding(value) + # Enable/Disable IPv6 forwarding + tmp = dict_search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + vrf_if.set_ipv6_forwarding(value) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index 070a4deea..a52c52706 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -155,7 +155,7 @@ def get_local_from(zone_policy, local_zone_name): def cleanup_commands(): commands = [] for table in ['ip filter', 'ip6 filter']: - json_str = cmd(f'nft -j list table {table}') + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) if 'nftables' not in obj: continue diff --git a/src/etc/cron.d/vyos-geoip b/src/etc/cron.d/vyos-geoip new file mode 100644 index 000000000..9bb38a850 --- /dev/null +++ b/src/etc/cron.d/vyos-geoip @@ -0,0 +1 @@ +30 4 * * 1 root sg vyattacfg "/usr/libexec/vyos/geoip-update.py --force" >/tmp/geoip-update.log 2>&1 diff --git a/src/etc/cron.hourly/vyos-logrotate-hourly b/src/etc/cron.hourly/vyos-logrotate-hourly deleted file mode 100755 index f4f56a9c2..000000000 --- a/src/etc/cron.hourly/vyos-logrotate-hourly +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -test -x /usr/sbin/logrotate || exit 0 -/usr/sbin/logrotate /etc/logrotate.conf diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index 74a7e83bf..5d879471d 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -26,7 +26,7 @@ function iptovtysh () { local VTYSH_GATEWAY="" local VTYSH_DEV="" local VTYSH_TAG="210" - local VTYSH_DISTANCE="" + local VTYSH_DISTANCE=$IF_METRIC # convert default route to 0.0.0.0/0 if [ "$4" == "default" ] ; then VTYSH_NETADDR="0.0.0.0/0" diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index e03d3a29c..4feb7e09a 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -27,6 +27,12 @@ net.ipv4.conf.all.arp_announce=2 # Enable packet forwarding for IPv4 net.ipv4.ip_forward=1 +# Enable directed broadcast forwarding feature described in rfc1812#section-5.3.5.2 and rfc2644. +# Note that setting the 'all' entry to 1 doesn't enable directed broadcast forwarding on all interfaces. +# To enable directed broadcast forwarding on an interface, both the 'all' entry and the input interface entry should be set to 1. +net.ipv4.conf.all.bc_forwarding=1 +net.ipv4.conf.default.bc_forwarding=0 + # if a primary address is removed from an interface promote the # secondary address if available net.ipv4.conf.all.promote_secondaries=1 diff --git a/src/etc/systemd/system/logrotate.timer.d/10-override.conf b/src/etc/systemd/system/logrotate.timer.d/10-override.conf new file mode 100644 index 000000000..f50c2b082 --- /dev/null +++ b/src/etc/systemd/system/logrotate.timer.d/10-override.conf @@ -0,0 +1,2 @@ +[Timer] +OnCalendar=hourly diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py new file mode 100755 index 000000000..34accf2cc --- /dev/null +++ b/src/helpers/geoip-update.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 sys + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import geoip_update + +def get_config(config=None): + if config: + conf = config + else: + conf = ConfigTreeQuery() + base = ['firewall'] + + if not conf.exists(base): + return None + + return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--force", help="Force update", action="store_true") + args = parser.parse_args() + + firewall = get_config() + + if not geoip_update(firewall, force=args.force): + sys.exit(1) diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py new file mode 100755 index 000000000..6b677670b --- /dev/null +++ b/src/helpers/vyos-domain-group-resolve.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 time + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements +from vyos.util import call + + +base = ['firewall', 'group', 'domain-group'] +check_required = True +# count_failed = 0 +# Timeout in sec between checks +timeout = 300 + +domain_state = {} + +if __name__ == '__main__': + + while check_required: + config = ConfigTreeQuery() + if config.exists(base): + domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + for set_name, domain_config in domain_groups.items(): + list_domains = domain_config['address'] + elements = [] + ip_dict = get_ips_domains_dict(list_domains) + + for domain in list_domains: + # Resolution succeeded, update domain state + if domain in ip_dict: + domain_state[domain] = ip_dict[domain] + elements += ip_dict[domain] + # Resolution failed, use previous domain state + elif domain in domain_state: + elements += domain_state[domain] + + # Resolve successful + if elements: + nft_update_set_elements(f'D_{set_name}', elements) + time.sleep(timeout) diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 index 5f4cff90d..626d6849f 100755 --- a/src/migration-scripts/firewall/6-to-7 +++ b/src/migration-scripts/firewall/6-to-7 @@ -194,11 +194,12 @@ if config.exists(base + ['ipv6-name']): if config.exists(rule_icmp + ['type']): tmp = config.return_value(rule_icmp + ['type']) - type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) + type_code_match = re.match(r'^(\d+)(?:/(\d+))?$', tmp) if type_code_match: config.set(rule_icmp + ['type'], value=type_code_match[1]) - config.set(rule_icmp + ['code'], value=type_code_match[2]) + if type_code_match[2]: + config.set(rule_icmp + ['code'], value=type_code_match[2]) elif tmp in icmpv6_remove: config.delete(rule_icmp + ['type']) elif tmp in icmpv6_translations: diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 93ce9215f..4095f2a3e 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -20,6 +20,7 @@ import os import sys from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN from vyos.pki import load_certificate from vyos.pki import load_crl from vyos.pki import load_dh_parameters @@ -27,6 +28,7 @@ from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_dh_parameters from vyos.pki import encode_private_key +from vyos.pki import verify_crl from vyos.util import run def wrapped_pem_to_config_value(pem): @@ -129,6 +131,8 @@ if config.exists(base): config.delete(base + [interface, 'tls', 'crypt-file']) + ca_certs = {} + if config.exists(x509_base + ['ca-cert-file']): if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) @@ -136,20 +140,27 @@ if config.exists(base): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) + certs_str = f.read() + certs_data = certs_str.split(CERT_BEGIN) + index = 1 + for cert_data in certs_data[1:]: + cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + + if cert: + ca_certs[f'{pki_name}_{index}'] = cert + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + index += 1 else: print(f'Failed to migrate CA certificate on openvpn interface {interface}') @@ -163,6 +174,7 @@ if config.exists(base): crl_file = config.return_value(x509_base + ['crl-file']) crl_path = os.path.join(AUTH_DIR, crl_file) crl = None + crl_ca_name = None if os.path.isfile(crl_path): if not os.access(crl_path, os.R_OK): @@ -172,9 +184,14 @@ if config.exists(base): crl_data = f.read() crl = load_crl(crl_data, wrap_tags=False) - if crl: + for ca_name, ca_cert in ca_certs.items(): + if verify_crl(crl, ca_cert): + crl_ca_name = ca_name + break + + if crl and crl_ca_name: crl_pem = encode_certificate(crl) - config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) else: print(f'Failed to migrate CRL on openvpn interface {interface}') diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6 index e9adee01b..3a8b3926d 100755 --- a/src/migration-scripts/ipsec/5-to-6 +++ b/src/migration-scripts/ipsec/5-to-6 @@ -78,7 +78,7 @@ if config.exists(log_mode): base_interfaces = base + ['ipsec-interfaces', 'interface'] if config.exists(base_interfaces): config.copy(base_interfaces, base + ['interface']) - config.delete(base_interfaces) + config.delete(base + ['ipsec-interfaces']) # Remove deprecated "auto-update" option tmp = base + ['auto-update'] diff --git a/src/migration-scripts/monitoring/0-to-1 b/src/migration-scripts/monitoring/0-to-1 new file mode 100755 index 000000000..803cdb49c --- /dev/null +++ b/src/migration-scripts/monitoring/0-to-1 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'monitoring', 'telegraf'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['authentication', 'organization']): + tmp = config.return_value(base + ['authentication', 'organization']) + config.delete(base + ['authentication', 'organization']) + config.set(base + ['influxdb', 'authentication', 'organization'], value=tmp) + +if config.exists(base + ['authentication', 'token']): + tmp = config.return_value(base + ['authentication', 'token']) + config.delete(base + ['authentication', 'token']) + config.set(base + ['influxdb', 'authentication', 'token'], value=tmp) + +if config.exists(base + ['bucket']): + tmp = config.return_value(base + ['bucket']) + config.delete(base + ['bucket']) + config.set(base + ['influxdb', 'bucket'], value=tmp) + +if config.exists(base + ['port']): + tmp = config.return_value(base + ['port']) + config.delete(base + ['port']) + config.set(base + ['influxdb', 'port'], value=tmp) + +if config.exists(base + ['url']): + tmp = config.return_value(base + ['url']) + config.delete(base + ['url']) + config.set(base + ['influxdb', 'url'], value=tmp) + + +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/policy/2-to-3 b/src/migration-scripts/policy/2-to-3 new file mode 100755 index 000000000..84cb1ff4a --- /dev/null +++ b/src/migration-scripts/policy/2-to-3 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T3976: change cli +# from: set policy route-map FOO rule 10 match ipv6 nexthop 'h:h:h:h:h:h:h:h' +# to: set policy route-map FOO rule 10 match ipv6 nexthop address 'h:h:h:h:h:h:h:h' + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['policy', 'route-map'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +for route_map in config.list_nodes(base): + if not config.exists(base + [route_map, 'rule']): + continue + for rule in config.list_nodes(base + [route_map, 'rule']): + base_rule = base + [route_map, 'rule', rule] + + if config.exists(base_rule + ['match', 'ipv6', 'nexthop']): + tmp = config.return_value(base_rule + ['match', 'ipv6', 'nexthop']) + config.delete(base_rule + ['match', 'ipv6', 'nexthop']) + config.set(base_rule + ['match', 'ipv6', 'nexthop', 'address'], value=tmp) + +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)
\ No newline at end of file diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24 index 5ea71d51a..97fe82462 100755 --- a/src/migration-scripts/system/23-to-24 +++ b/src/migration-scripts/system/23-to-24 @@ -20,6 +20,7 @@ from ipaddress import ip_interface from ipaddress import ip_address from sys import exit, argv from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 if (len(argv) < 1): print("Must specify file name!") @@ -37,6 +38,9 @@ def fixup_cli(config, path, interface): if config.exists(path + ['address']): for address in config.return_values(path + ['address']): tmp = ip_interface(address) + # ARP is only available for IPv4 ;-) + if not is_ipv4(tmp): + continue if ip_address(host) in tmp.network.hosts(): mac = config.return_value(tmp_base + [host, 'hwaddr']) iface_path = ['protocols', 'static', 'arp', 'interface'] diff --git a/src/migration-scripts/system/24-to-25 b/src/migration-scripts/system/24-to-25 new file mode 100755 index 000000000..c2f70689d --- /dev/null +++ b/src/migration-scripts/system/24-to-25 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. +# +# Migrate system syslog global archive to system logs logrotate messages + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'syslog', 'global', 'archive'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +if config.exists(base + ['file']): + tmp = config.return_value(base + ['file']) + config.set(['system', 'logs', 'logrotate', 'messages', 'rotate'], value=tmp) + +if config.exists(base + ['size']): + tmp = config.return_value(base + ['size']) + tmp = max(round(int(tmp) / 1024), 1) # kb -> mb + config.set(['system', 'logs', 'logrotate', 'messages', 'max-size'], value=tmp) + +config.delete(base) + +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/vrf/0-to-1 b/src/migration-scripts/vrf/0-to-1 index 2b41ef3c7..5df751113 100755 --- a/src/migration-scripts/vrf/0-to-1 +++ b/src/migration-scripts/vrf/0-to-1 @@ -114,6 +114,16 @@ for vrf in config.list_nodes(base): if config.exists(vrf_path): config.rename(vrf_path, 'vrf') + next_hop = route_path + [route, 'interface'] + if config.exists(next_hop): + for interface in config.list_nodes(next_hop): + interface_path = next_hop + [interface, 'next-hop-interface'] + if config.exists(interface_path): + config.rename(interface_path, 'interface') + vrf_path = next_hop + [interface, 'next-hop-vrf'] + if config.exists(vrf_path): + config.rename(vrf_path, 'vrf') + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py new file mode 100755 index 000000000..250dbcce1 --- /dev/null +++ b/src/op_mode/clear_dhcp_lease.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import argparse +import re + +from isc_dhcp_leases import Lease +from isc_dhcp_leases import IscDhcpLeases + +from vyos.configquery import ConfigTreeQuery +from vyos.util import ask_yes_no +from vyos.util import call +from vyos.util import commit_in_progress + + +config = ConfigTreeQuery() +base = ['service', 'dhcp-server'] +lease_file = '/config/dhcpd.leases' + + +def del_lease_ip(address): + """ + Read lease_file and write data to this file + without specific section "lease ip" + Delete section "lease x.x.x.x { x;x;x; }" + """ + with open(lease_file, encoding='utf-8') as f: + data = f.read().rstrip() + lease_config_ip = '{(?P<config>[\s\S]+?)\n}' + pattern = rf"lease {address} {lease_config_ip}" + # Delete lease for ip block + data = re.sub(pattern, '', data) + + # Write new data to original lease_file + with open(lease_file, 'w', encoding='utf-8') as f: + f.write(data) + +def is_ip_in_leases(address): + """ + Return True if address found in the lease file + """ + leases = IscDhcpLeases(lease_file) + lease_ips = [] + for lease in leases.get(): + lease_ips.append(lease.ip) + if address not in lease_ips: + print(f'Address "{address}" not found in "{lease_file}"') + return False + return True + + +if not config.exists(base): + print('DHCP-server not configured!') + exit(0) + +if config.exists(base + ['failover']): + print('Lease cannot be reset in failover mode!') + exit(0) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--ip', help='IPv4 address', action='store', required=True) + + args = parser.parse_args() + address = args.ip + + if not is_ip_in_leases(address): + exit(1) + + if commit_in_progress(): + print('Cannot clear DHCP lease while a commit is in progress') + exit(1) + + if not ask_yes_no(f'This will restart DHCP server.\nContinue?'): + exit(1) + else: + del_lease_ip(address) + call('systemctl restart isc-dhcp-server.service') diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index ffc574362..936c20bcb 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -20,6 +20,7 @@ import argparse from psutil import process_iter from vyos.util import call +from vyos.util import commit_in_progress from vyos.util import DEVNULL from vyos.util import is_wwan_connected @@ -87,6 +88,9 @@ def main(): args = parser.parse_args() if args.connect: + if commit_in_progress(): + print('Cannot connect while a commit is in progress') + exit(1) connect(args.connect) elif args.disconnect: disconnect(args.disconnect) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index e45c38f07..54ecd6d0e 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -22,6 +22,7 @@ from argparse import ArgumentParser from vyos.configquery import CliShellApiConfigQuery from vyos.configquery import ConfigTreeQuery from vyos.util import call +from vyos.util import commit_in_progress from vyos.util import cmd from vyos.util import run from vyos.template import render_to_string @@ -86,6 +87,9 @@ if __name__ == '__main__': if args.restart: is_configured() + if commit_in_progress(): + print('Cannot restart conntrackd while a commit is in progress') + exit(1) syslog.syslog('Restarting conntrack sync service...') cmd('systemctl restart conntrackd.service') diff --git a/src/op_mode/containers_op.py b/src/op_mode/containers_op.py deleted file mode 100755 index c55a48b3c..000000000 --- a/src/op_mode/containers_op.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-2022 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 - -from getpass import getuser -from vyos.configquery import ConfigTreeQuery -from vyos.base import Warning -from vyos.util import cmd -from subprocess import STDOUT - -parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Show all containers") -parser.add_argument("-i", "--image", action="store_true", help="Show container images") -parser.add_argument("-n", "--networks", action="store_true", help="Show container images") -parser.add_argument("-p", "--pull", action="store", help="Pull image for container") -parser.add_argument("-d", "--remove", action="store", help="Delete container image") -parser.add_argument("-u", "--update", action="store", help="Update given container image") - -config = ConfigTreeQuery() -base = ['container'] - -if getuser() != 'root': - raise OSError('This functions needs to be run as root to return correct results!') - -if __name__ == '__main__': - args = parser.parse_args() - - if args.all: - print(cmd('podman ps --all')) - elif args.image: - print(cmd('podman image ls')) - elif args.networks: - print(cmd('podman network ls')) - - elif args.pull: - image = args.pull - registry_config = '/etc/containers/registries.conf' - if not os.path.exists(registry_config): - Warning('No container registry configured. Please use full URL when '\ - 'adding an image. E.g. prefix with docker.io/image-name.') - try: - print(os.system(f'podman image pull {image}')) - except Exception as e: - print(f'Unable to download image "{image}". {e}') - - elif args.remove: - image = args.remove - try: - print(os.system(f'podman image rm {image}')) - except FileNotFoundError as e: - print(f'Unable to delete image "{image}". {e}') - - elif args.update: - tmp = config.get_config_dict(base + ['name', args.update], - key_mangling=('-', '_'), get_first_key=True) - try: - image = tmp['image'] - print(cmd(f'podman image pull {image}')) - except Exception as e: - print(f'Unable to download image "{image}". {e}') - else: - parser.print_help() - exit(1) - - exit(0) diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 3146fc357..0aea17b3a 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -270,7 +270,7 @@ def show_firewall_group(name=None): references = find_references(group_type, group_name) row = [group_name, group_type, '\n'.join(references) or 'N/A'] if 'address' in group_conf: - row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address))) + row.append("\n".join(sorted(group_conf['address']))) elif 'network' in group_conf: row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) elif 'mac_address' in group_conf: diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 6586cbceb..514143cd7 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -22,7 +22,9 @@ import ipaddress import os.path from tabulate import tabulate from json import loads -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import commit_in_progress +from vyos.util import run from vyos.logger import syslog # some default values @@ -224,6 +226,9 @@ if not _uacctd_running(): # restart pmacct daemon if cmd_args.action == 'restart': + if commit_in_progress(): + print('Cannot restart flow-accounting while a commit is in progress') + exit(1) # run command to restart flow-accounting cmd('systemctl restart uacctd.service', message='Failed to restart flow-accounting') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index cbc9ef973..43e94048d 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -17,10 +17,15 @@ from sys import exit from vyos.util import ask_yes_no from vyos.util import cmd +from vyos.util import commit_in_progress if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): exit(0) +if commit_in_progress(): + print('Cannot restart SSH while a commit is in progress') + exit(1) + cmd('rm -v /etc/ssh/ssh_host_*') cmd('dpkg-reconfigure openssh-server') cmd('systemctl restart ssh.service') diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py index c3cd25186..a128cc011 100755 --- a/src/op_mode/openconnect-control.py +++ b/src/op_mode/openconnect-control.py @@ -19,7 +19,10 @@ import argparse import json from vyos.config import Config -from vyos.util import popen, run, DEVNULL +from vyos.util import commit_in_progress +from vyos.util import popen +from vyos.util import run +from vyos.util import DEVNULL from tabulate import tabulate occtl = '/usr/bin/occtl' @@ -57,6 +60,10 @@ def main(): # Check is Openconnect server configured is_ocserv_configured() + if commit_in_progress(): + print('Cannot restart openconnect while a commit is in progress') + exit(1) + if args.action == "restart": run("sudo systemctl restart ocserv.service") sys.exit(0) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index bc7813052..1e78c3a03 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -17,6 +17,7 @@ import argparse import ipaddress import os +import re import sys import tabulate @@ -30,7 +31,8 @@ from vyos.pki import encode_certificate, encode_public_key, encode_private_key, from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list from vyos.pki import create_private_key from vyos.pki import create_dh_parameters -from vyos.pki import load_certificate, load_certificate_request, load_private_key, load_crl +from vyos.pki import load_certificate, load_certificate_request, load_private_key +from vyos.pki import load_crl, load_dh_parameters, load_public_key from vyos.pki import verify_certificate from vyos.xml import defaults from vyos.util import ask_input, ask_yes_no @@ -183,13 +185,13 @@ def install_ssh_key(name, public_key, private_key, passphrase=None): ]) print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) -def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None): +def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None, prompt=True): # Show/install conf commands for key-pair config_paths = [] if public_key: - install_public_key = ask_yes_no('Do you want to install the public key?', default=True) + install_public_key = not prompt or ask_yes_no('Do you want to install the public key?', default=True) public_key_pem = encode_public_key(public_key) if install_public_key: @@ -200,7 +202,7 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras print(public_key_pem) if private_key: - install_private_key = ask_yes_no('Do you want to install the private key?', default=True) + install_private_key = not prompt or ask_yes_no('Do you want to install the private key?', default=True) private_key_pem = encode_private_key(private_key, passphrase=passphrase) if install_private_key: @@ -214,6 +216,13 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras install_into_config(conf, config_paths) +def install_openvpn_key(name, key_data, key_version='1'): + config_paths = [ + f"pki openvpn shared-secret {name} key '{key_data}'", + f"pki openvpn shared-secret {name} version '{key_version}'" + ] + install_into_config(conf, config_paths) + def install_wireguard_key(interface, private_key, public_key): # Show conf commands for installing wireguard key pairs from vyos.ifconfig import Section @@ -640,15 +649,11 @@ def generate_openvpn_key(name, install=False, file=False): key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings key_version = '1' - import re version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully) if version_search: key_version = version_search[1] - base = f"set pki openvpn shared-secret {name}" - print("Configure mode commands to install OpenVPN key:") - print(f"{base} key '{key_data}'") - print(f"{base} version '{key_version}'") + install_openvpn_key(name, key_data, key_version) if file: write_file(f'{name}.key', result) @@ -670,6 +675,167 @@ def generate_wireguard_psk(interface=None, peer=None, install=False): else: print(f'Pre-shared key: {psk}') +# Import functions +def import_ca_certificate(name, path=None, key_path=None): + if path: + if not os.path.exists(path): + print(f'File not found: {path}') + return + + cert = None + + with open(path) as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if not cert: + print(f'Invalid certificate: {path}') + return + + install_certificate(name, cert, is_ca=True) + + if key_path: + if not os.path.exists(key_path): + print(f'File not found: {key_path}') + return + + key = None + passphrase = ask_input('Enter private key passphrase: ') or None + + with open(key_path) as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) + + if not key: + print(f'Invalid private key or passphrase: {path}') + return + + install_certificate(name, private_key=key, is_ca=True) + +def import_certificate(name, path=None, key_path=None): + if path: + if not os.path.exists(path): + print(f'File not found: {path}') + return + + cert = None + + with open(path) as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if not cert: + print(f'Invalid certificate: {path}') + return + + install_certificate(name, cert, is_ca=False) + + if key_path: + if not os.path.exists(key_path): + print(f'File not found: {key_path}') + return + + key = None + passphrase = ask_input('Enter private key passphrase: ') or None + + with open(key_path) as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) + + if not key: + print(f'Invalid private key or passphrase: {path}') + return + + install_certificate(name, private_key=key, is_ca=False) + +def import_crl(name, path): + if not os.path.exists(path): + print(f'File not found: {path}') + return + + crl = None + + with open(path) as f: + crl_data = f.read() + crl = load_crl(crl_data, wrap_tags=False) + + if not crl: + print(f'Invalid certificate: {path}') + return + + install_crl(name, crl) + +def import_dh_parameters(name, path): + if not os.path.exists(path): + print(f'File not found: {path}') + return + + dh = None + + with open(path) as f: + dh_data = f.read() + dh = load_dh_parameters(dh_data, wrap_tags=False) + + if not dh: + print(f'Invalid DH parameters: {path}') + return + + install_dh_parameters(name, dh) + +def import_keypair(name, path=None, key_path=None): + if path: + if not os.path.exists(path): + print(f'File not found: {path}') + return + + key = None + + with open(path) as f: + key_data = f.read() + key = load_public_key(key_data, wrap_tags=False) + + if not key: + print(f'Invalid public key: {path}') + return + + install_keypair(name, None, public_key=key, prompt=False) + + if key_path: + if not os.path.exists(key_path): + print(f'File not found: {key_path}') + return + + key = None + passphrase = ask_input('Enter private key passphrase: ') or None + + with open(key_path) as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) + + if not key: + print(f'Invalid private key or passphrase: {path}') + return + + install_keypair(name, None, private_key=key, prompt=False) + +def import_openvpn_secret(name, path): + if not os.path.exists(path): + print(f'File not found: {path}') + return + + key_data = None + key_version = '1' + + with open(path) as f: + key_lines = f.read().split("\n") + key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings + + version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully) + if version_search: + key_version = version_search[1] + + install_openvpn_key(name, key_data, key_version) + # Show functions def show_certificate_authority(name=None): headers = ['Name', 'Subject', 'Issuer CN', 'Issued', 'Expiry', 'Private Key', 'Parent'] @@ -799,6 +965,9 @@ if __name__ == '__main__': parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true') parser.add_argument('--install', help='Install generated keys into running-config', action='store_true') + parser.add_argument('--filename', help='Write certificate into specified filename', action='store') + parser.add_argument('--key-filename', help='Write key into specified filename', action='store') + args = parser.parse_args() try: @@ -840,7 +1009,19 @@ if __name__ == '__main__': generate_wireguard_key(args.interface, install=args.install) if args.psk: generate_wireguard_psk(args.interface, peer=args.peer, install=args.install) - + elif args.action == 'import': + if args.ca: + import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename) + elif args.certificate: + import_certificate(args.certificate, path=args.filename, key_path=args.key_filename) + elif args.crl: + import_crl(args.crl, args.filename) + elif args.dh: + import_dh_parameters(args.dh, args.filename) + elif args.keypair: + import_keypair(args.keypair, path=args.filename, key_path=args.key_filename) + elif args.openvpn: + import_openvpn_secret(args.openvpn, args.filename) elif args.action == 'show': if args.ca: ca_name = None if args.ca == 'all' else args.ca diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index dbd3eb4d1..efbf65083 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -17,6 +17,7 @@ import os from sys import argv, exit from vyos.util import call +from vyos.util import commit_in_progress if __name__ == '__main__': if (len(argv) < 1): @@ -25,6 +26,9 @@ if __name__ == '__main__': interface = argv[1] if os.path.isfile(f'/run/openvpn/{interface}.conf'): + if commit_in_progress(): + print('Cannot restart OpenVPN while a commit is in progress') + exit(1) call(f'systemctl restart openvpn@{interface}.service') else: print(f'OpenVPN interface "{interface}" does not exist!') diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index af4fb2d15..db5a48970 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -24,6 +24,7 @@ import os import vyos.config from vyos.util import call +from vyos.util import commit_in_progress parser = argparse.ArgumentParser() @@ -39,6 +40,9 @@ if __name__ == '__main__': if not c.exists_effective('service dhcp-relay'): print("DHCP relay service not configured") else: + if commit_in_progress(): + print('Cannot restart DHCP relay while a commit is in progress') + exit(1) call('systemctl restart isc-dhcp-server.service') sys.exit(0) @@ -47,6 +51,9 @@ if __name__ == '__main__': if not c.exists_effective('service dhcpv6-relay'): print("DHCPv6 relay service not configured") else: + if commit_in_progress(): + print('Cannot restart DHCPv6 relay while commit is in progress') + exit(1) call('systemctl restart isc-dhcp-server6.service') sys.exit(0) diff --git a/src/op_mode/show_conntrack.py b/src/op_mode/show_conntrack.py new file mode 100755 index 000000000..4eb160d97 --- /dev/null +++ b/src/op_mode/show_conntrack.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 xmltodict + +from tabulate import tabulate +from vyos.util import cmd + + +def _get_raw_data(): + """ + Get conntrack XML output + """ + return cmd(f'sudo conntrack --dump --output xml') + + +def _xml_to_dict(xml): + """ + Convert XML to dictionary + Return: dictionary + """ + parse = xmltodict.parse(xml) + # If only one conntrack entry we must change dict + if 'meta' in parse['conntrack']['flow']: + return dict(conntrack={'flow': [parse['conntrack']['flow']]}) + return parse + + +def _get_formatted_output(xml): + """ + :param xml: + :return: formatted output + """ + data_entries = [] + dict_data = _xml_to_dict(xml) + for entry in dict_data['conntrack']['flow']: + src, dst, sport, dport, proto = {}, {}, {}, {}, {} + for meta in entry['meta']: + direction = meta['@direction'] + if direction in ['original']: + if 'layer3' in meta: + src = meta['layer3']['src'] + dst = meta['layer3']['dst'] + if 'layer4' in meta: + if meta.get('layer4').get('sport'): + sport = meta['layer4']['sport'] + if meta.get('layer4').get('dport'): + dport = meta['layer4']['dport'] + proto = meta['layer4']['@protoname'] + if direction == 'independent': + conn_id = meta['id'] + timeout = meta['timeout'] + src = f'{src}:{sport}' if sport else src + dst = f'{dst}:{dport}' if dport else dst + state = meta['state'] if 'state' in meta else '' + data_entries.append([conn_id, src, dst, proto, state, timeout]) + headers = ["Connection id", "Source", "Destination", "Protocol", "State", "Timeout"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def show(raw: bool): + conntrack_data = _get_raw_data() + if raw: + return conntrack_data + else: + return _get_formatted_output(conntrack_data) + + +if __name__ == '__main__': + print(show(raw=False)) diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py index 25091e9fc..508845e23 100755 --- a/src/op_mode/show_nat_translations.py +++ b/src/op_mode/show_nat_translations.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -83,11 +83,23 @@ def pipe(): return xml +def xml_to_dict(xml): + """ + Convert XML to dictionary + Return: dictionary + """ + parse = xmltodict.parse(xml) + # If only one NAT entry we must change dict T4499 + if 'meta' in parse['conntrack']['flow']: + return dict(conntrack={'flow': [parse['conntrack']['flow']]}) + return parse + + def process(data, stats, protocol, pipe, verbose, flowtype=''): if not data: return - parsed = xmltodict.parse(data) + parsed = xml_to_dict(data) print(headers(verbose, pipe)) diff --git a/src/op_mode/show_neigh.py b/src/op_mode/show_neigh.py index 94e745493..d874bd544 100755 --- a/src/op_mode/show_neigh.py +++ b/src/op_mode/show_neigh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2022 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,83 +14,89 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -#ip -j -f inet neigh list | jq -#[ - #{ - #"dst": "192.168.101.8", - #"dev": "enp0s25", - #"lladdr": "78:d2:94:72:77:7e", - #"state": [ - #"STALE" - #] - #}, - #{ - #"dst": "192.168.101.185", - #"dev": "enp0s25", - #"lladdr": "34:46:ec:76:f8:9b", - #"state": [ - #"STALE" - #] - #}, - #{ - #"dst": "192.168.101.225", - #"dev": "enp0s25", - #"lladdr": "c2:cb:fa:bf:a0:35", - #"state": [ - #"STALE" - #] - #}, - #{ - #"dst": "192.168.101.1", - #"dev": "enp0s25", - #"lladdr": "00:98:2b:f8:3f:11", - #"state": [ - #"REACHABLE" - #] - #}, - #{ - #"dst": "192.168.101.181", - #"dev": "enp0s25", - #"lladdr": "d8:9b:3b:d5:88:22", - #"state": [ - #"STALE" - #] - #} -#] +# Sample output of `ip --json neigh list`: +# +# [ +# { +# "dst": "192.168.1.1", +# "dev": "eth0", # Missing if `dev ...` option is used +# "lladdr": "00:aa:bb:cc:dd:ee", # May be missing for failed entries +# "state": [ +# "REACHABLE" +# ] +# }, +# ] import sys -import argparse -import json -from vyos.util import cmd - -def main(): - #parese args - parser = argparse.ArgumentParser() - parser.add_argument('--family', help='Protocol family', required=True) - args = parser.parse_args() - - neigh_raw_json = cmd(f'ip -j -f {args.family} neigh list') - neigh_raw_json = neigh_raw_json.lower() - neigh_json = json.loads(neigh_raw_json) - - format_neigh = '%-50s %-10s %-20s %s' - print(format_neigh % ("IP Address", "Device", "State", "LLADDR")) - print(format_neigh % ("----------", "------", "-----", "------")) - - if neigh_json is not None: - for neigh_item in neigh_json: - dev = neigh_item['dev'] - dst = neigh_item['dst'] - lladdr = neigh_item['lladdr'] if 'lladdr' in neigh_item else '' - state = neigh_item['state'] - - i = 0 - for state_item in state: - if i == 0: - print(format_neigh % (dst, dev, state_item, lladdr)) - else: - print(format_neigh % ('', '', state_item, '')) - i+=1 - + + +def get_raw_data(family, device=None, state=None): + from json import loads + from vyos.util import cmd + + if device: + device = f"dev {device}" + else: + device = "" + + if state: + state = f"nud {state}" + else: + state = "" + + neigh_cmd = f"ip --family {family} --json neighbor list {device} {state}" + + data = loads(cmd(neigh_cmd)) + + return data + +def get_formatted_output(family, device=None, state=None): + from tabulate import tabulate + + def entry_to_list(e, intf=None): + dst = e["dst"] + + # State is always a list in the iproute2 output + state = ", ".join(e["state"]) + + # Link layer address is absent from e.g. FAILED entries + if "lladdr" in e: + lladdr = e["lladdr"] + else: + lladdr = None + + # Device field is absent from outputs of `ip neigh list dev ...` + if "dev" in e: + dev = e["dev"] + elif device: + dev = device + else: + raise ValueError("interface is not defined") + + return [dst, dev, lladdr, state] + + neighs = get_raw_data(family, device=device, state=state) + neighs = map(entry_to_list, neighs) + + headers = ["Address", "Interface", "Link layer address", "State"] + return tabulate(neighs, headers) + if __name__ == '__main__': - main() + from argparse import ArgumentParser + + parser = ArgumentParser() + parser.add_argument("-f", "--family", type=str, default="inet", help="Address family") + parser.add_argument("-i", "--interface", type=str, help="Network interface") + parser.add_argument("-s", "--state", type=str, help="Neighbor table entry state") + + args = parser.parse_args() + + if args.state: + if args.state not in ["reachable", "failed", "stale", "permanent"]: + raise ValueError(f"""Incorrect state "{args.state}"! Must be one of: reachable, stale, failed, permanent""") + + try: + print(get_formatted_output(args.family, device=args.interface, state=args.state)) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_openconnect_otp.py b/src/op_mode/show_openconnect_otp.py new file mode 100755 index 000000000..ae532ccc9 --- /dev/null +++ b/src/op_mode/show_openconnect_otp.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# Copyright 2017, 2022 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 argparse +import os + +from vyos.config import Config +from vyos.xml import defaults +from vyos.configdict import dict_merge +from vyos.util import popen +from base64 import b32encode + +otp_file = '/run/ocserv/users.oath' + +def check_uname_otp(username): + """ + Check if "username" exists and have an OTP key + """ + config = Config() + base_key = ['vpn', 'openconnect', 'authentication', 'local-users', 'username', username, 'otp', 'key'] + if not config.exists(base_key): + return None + return True + +def get_otp_ocserv(username): + config = Config() + base = ['vpn', 'openconnect'] + if not config.exists(base): + return None + ocserv = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + ocserv = dict_merge(default_values, ocserv) + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + del ocserv['authentication']['local_users']['username']['otp'] + if not ocserv["authentication"]["local_users"]["username"]: + return None + default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] + for user, params in ocserv['authentication']['local_users']['username'].items(): + # Not every configuration requires OTP settings + if ocserv['authentication']['local_users']['username'][user].get('otp'): + ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) + result = ocserv['authentication']['local_users']['username'][username] + return result + +def display_otp_ocserv(username, params, info): + hostname = os.uname()[1] + key_hex = params['otp']['key'] + otp_length = params['otp']['otp_length'] + interval = params['otp']['interval'] + token_type = params['otp']['token_type'] + if token_type == 'hotp-time': + token_type_acrn = 'totp' + key_base32 = b32encode(bytes.fromhex(key_hex)).decode() + otp_url = ''.join(["otpauth://",token_type_acrn,"/",username,"@",hostname,"?secret=",key_base32,"&digits=",otp_length,"&period=",interval]) + qrcode,err = popen('qrencode -t ansiutf8', input=otp_url) + + if info == 'full': + print("# You can share it with the user, he just needs to scan the QR in his OTP app") + print("# username: ", username) + print("# OTP KEY: ", key_base32) + print("# OTP URL: ", otp_url) + print(qrcode) + print('# To add this OTP key to configuration, run the following commands:') + print(f"set vpn openconnect authentication local-users username {username} otp key '{key_hex}'") + if interval != "30": + print(f"set vpn openconnect authentication local-users username {username} otp interval '{interval}'") + if otp_length != "6": + print(f"set vpn openconnect authentication local-users username {username} otp otp-length '{otp_length}'") + elif info == 'key-hex': + print("# OTP key in hexadecimal: ") + print(key_hex) + elif info == 'key-b32': + print("# OTP key in Base32: ") + print(key_base32) + elif info == 'qrcode': + print(f"# QR code for OpenConnect user '{username}'") + print(qrcode) + elif info == 'uri': + print(f"# URI for OpenConnect user '{username}'") + print(otp_url) + +if __name__ == '__main__': + parser = argparse.ArgumentParser(add_help=False, description='Show OTP authentication information for selected user') + parser.add_argument('--user', action="store", type=str, default='', help='Username') + parser.add_argument('--info', action="store", type=str, default='full', help='Wich information to display') + + args = parser.parse_args() + check_otp = check_uname_otp(args.user) + if check_otp: + user_otp_params = get_otp_ocserv(args.user) + display_otp_ocserv(args.user, user_otp_params, args.info) + else: + print(f'There is no such user ("{args.user}") with an OTP key configured') diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py index 1b5e33fa9..b70c60cf8 100755 --- a/src/op_mode/show_uptime.py +++ b/src/op_mode/show_uptime.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 as @@ -26,14 +26,17 @@ def get_uptime_seconds(): def get_load_averages(): from re import search from vyos.util import cmd + from vyos.cpu import get_core_count data = cmd("uptime") matches = search(r"load average:\s*(?P<one>[0-9\.]+)\s*,\s*(?P<five>[0-9\.]+)\s*,\s*(?P<fifteen>[0-9\.]+)\s*", data) + core_count = get_core_count() + res = {} - res[1] = float(matches["one"]) - res[5] = float(matches["five"]) - res[15] = float(matches["fifteen"]) + res[1] = float(matches["one"]) / core_count + res[5] = float(matches["five"]) / core_count + res[15] = float(matches["fifteen"]) / core_count return res @@ -53,9 +56,9 @@ def get_formatted_output(): out = "Uptime: {}\n\n".format(data["uptime"]) avgs = data["load_average"] out += "Load averages:\n" - out += "1 minute: {:.02f}%\n".format(avgs[1]*100) - out += "5 minutes: {:.02f}%\n".format(avgs[5]*100) - out += "15 minutes: {:.02f}%\n".format(avgs[15]*100) + out += "1 minute: {:.01f}%\n".format(avgs[1]*100) + out += "5 minutes: {:.01f}%\n".format(avgs[5]*100) + out += "15 minutes: {:.01f}%\n".format(avgs[15]*100) return out diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh index 09980e14f..25d09ce77 100755 --- a/src/op_mode/vtysh_wrapper.sh +++ b/src/op_mode/vtysh_wrapper.sh @@ -1,5 +1,6 @@ #!/bin/sh declare -a tmp -# FRR uses ospf6 where we use ospfv3, thus alter the command -tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/") +# FRR uses ospf6 where we use ospfv3, and we use reset over clear for BGP, +# thus alter the commands +tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/") vtysh -c "$tmp" diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index c1b595412..e9b904ba8 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -201,6 +201,20 @@ class ShowModel(ApiModel): } } +class ResetModel(ApiModel): + op: StrictStr + path: List[StrictStr] + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "reset", + "path": ["op", "mode", "path"], + } + } + + class Success(BaseModel): success: bool data: Union[str, bool, Dict] @@ -372,7 +386,7 @@ class MultipartRoute(APIRoute): return error(400, "Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(request.offending_command))) if request.ERR_PATH_NOT_LIST_OF_STR: return error(400, "Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(request.offending_command))) - if endpoint in ('/retrieve','/generate','/show'): + if endpoint in ('/retrieve','/generate','/show','/reset'): if request.ERR_NO_OP or request.ERR_NO_PATH: return error(400, "Missing required field. \"op\" and \"path\" fields are required") if endpoint in ('/config-file', '/image'): @@ -607,6 +621,27 @@ def show_op(data: ShowModel): return success(res) +@app.post('/reset') +def reset_op(data: ResetModel): + session = app.state.vyos_session + + op = data.op + path = data.path + + try: + if op == 'reset': + res = session.reset(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + logger.critical(traceback.format_exc()) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + + ### # GraphQL integration ### diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py new file mode 100755 index 000000000..1c85380bc --- /dev/null +++ b/src/system/vyos-event-handler.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 json +import re +import select +from copy import deepcopy +from os import getpid, environ +from pathlib import Path +from signal import signal, SIGTERM, SIGINT +from sys import exit +from systemd import journal + +from vyos.util import run, dict_search + +# Identify this script +my_pid = getpid() +my_name = Path(__file__).stem + + +# handle termination signal +def handle_signal(signal_type, frame): + if signal_type == SIGTERM: + journal.send('Received SIGTERM signal, stopping normally', + SYSLOG_IDENTIFIER=my_name) + if signal_type == SIGINT: + journal.send('Received SIGINT signal, stopping normally', + SYSLOG_IDENTIFIER=my_name) + exit(0) + + +# Class for analyzing and process messages +class Analyzer: + # Initialize settings + def __init__(self, config: dict) -> None: + self.config = {} + # Prepare compiled regex objects + for event_id, event_config in config.items(): + script = dict_search('script.path', event_config) + # Check for arguments + if dict_search('script.arguments', event_config): + script_arguments = dict_search('script.arguments', event_config) + script = f'{script} {script_arguments}' + # Prepare environment + environment = deepcopy(environ) + # Check for additional environment options + if dict_search('script.environment', event_config): + for env_variable, env_value in dict_search( + 'script.environment', event_config).items(): + environment[env_variable] = env_value.get('value') + # Create final config dictionary + pattern_raw = event_config['filter']['pattern'] + pattern_compiled = re.compile( + rf'{event_config["filter"]["pattern"]}') + pattern_config = { + pattern_compiled: { + 'pattern_raw': + pattern_raw, + 'syslog_id': + dict_search('filter.syslog-identifier', event_config), + 'pattern_script': { + 'path': script, + 'environment': environment + } + } + } + self.config.update(pattern_config) + + # Execute script safely + def script_run(self, pattern: str, script_path: str, + script_env: dict) -> None: + try: + run(script_path, env=script_env) + journal.send( + f'Pattern found: "{pattern}", script executed: "{script_path}"', + SYSLOG_IDENTIFIER=my_name) + except Exception as err: + journal.send( + f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}', + SYSLOG_IDENTIFIER=my_name) + + # Analyze a message + def process_message(self, message: dict) -> None: + for pattern_compiled, pattern_config in self.config.items(): + # Check if syslog id is presented in config and matches + syslog_id = pattern_config.get('syslog_id') + if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id: + continue + if pattern_compiled.fullmatch(message['MESSAGE']): + # Add message to environment variables + pattern_config['pattern_script']['environment'][ + 'message'] = message['MESSAGE'] + # Run script + self.script_run( + pattern=pattern_config['pattern_raw'], + script_path=pattern_config['pattern_script']['path'], + script_env=pattern_config['pattern_script']['environment']) + + +if __name__ == '__main__': + # Parse command arguments and get config + parser = argparse.ArgumentParser() + parser.add_argument('-c', + '--config', + action='store', + help='Path to even-handler configuration', + required=True, + type=Path) + + args = parser.parse_args() + try: + config_path = Path(args.config) + config = json.loads(config_path.read_text()) + # Create an object for analazyng messages + analyzer = Analyzer(config) + except Exception as err: + print( + f'Configuration file "{config_path}" does not exist or malformed: {err}' + ) + exit(1) + + # Prepare for proper exitting + signal(SIGTERM, handle_signal) + signal(SIGINT, handle_signal) + + # Set up journal connection + data = journal.Reader() + data.seek_tail() + data.get_previous() + p = select.poll() + p.register(data, data.get_events()) + + journal.send(f'Started with configuration: {config}', + SYSLOG_IDENTIFIER=my_name) + + while p.poll(): + if data.process() != journal.APPEND: + continue + for entry in data: + message = entry['MESSAGE'] + pid = entry['_PID'] + # Skip empty messages and messages from this process + if message and pid != my_pid: + try: + analyzer.process_message(entry) + except Exception as err: + journal.send(f'Unable to process message: {err}', + SYSLOG_IDENTIFIER=my_name) diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 2ced1038a..23cd4cfc3 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -13,6 +13,9 @@ PIDFile=/var/lib/dhcp/dhclient_%i.pid ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r Restart=always +TimeoutStopSec=20 +SendSIGKILL=true +FinalKillSignal=SIGABRT [Install] WantedBy=multi-user.target diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service new file mode 100644 index 000000000..29628fddb --- /dev/null +++ b/src/systemd/vyos-domain-group-resolve.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS firewall domain-group resolver +After=vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-event-handler.service b/src/systemd/vyos-event-handler.service new file mode 100644 index 000000000..6afe4f95b --- /dev/null +++ b/src/systemd/vyos-event-handler.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS event handler +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-event-handler.py --config /run/vyos-event-handler.conf + +[Install] +WantedBy=multi-user.target |