diff options
128 files changed, 3646 insertions, 350 deletions
| @@ -37,6 +37,11 @@ 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' 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/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index fac3fad03..1f88ae40c 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -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 {{ name }} { +        type ipv4_addr +        flags interval +    } +{%         endfor %} +{%     endif %}  {%     for set_name in ns.sets %}      set RECENT_{{ set_name }} {          type ipv4_addr 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 a42b73e98..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 }} 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/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/monitoring/telegraf.j2 b/data/templates/monitoring/telegraf.j2 index d1a94366b..a732fb5de 100644 --- a/data/templates/monitoring/telegraf.j2 +++ b/data/templates/monitoring/telegraf.j2 @@ -14,6 +14,23 @@    logfile = ""    hostname = ""    omit_hostname = false +{% 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_configured is vyos_defined %}  ### InfluxDB2 ###  [[outputs.influxdb_v2]] 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/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/debian/control b/debian/control index bcd5acfdd..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, @@ -160,6 +160,8 @@ Depends:    tftpd-hpa,    traceroute,    tuned, +  twamp-client, +  twamp-server,    udp-broadcast-relay,    uidmap,    usb-modeswitch, diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 60e738e01..6dabc5e1c 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -338,7 +338,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> 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..12dc11de5 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -140,7 +140,7 @@                          <children>                            <leafNode name="address">                              <properties> -                              <help>IPv4 address [REQUIRED]</help> +                              <help>IPv4 address</help>                                <valueHelp>                                  <format>ipv4</format>                                  <description>IPv4 address</description> @@ -173,7 +173,7 @@                          <children>                            <leafNode name="address">                              <properties> -                              <help>IPv6 address [REQUIRED]</help> +                              <help>IPv6 address</help>                                <valueHelp>                                  <format>ipv6</format>                                  <description>IPv6 address</description> @@ -206,7 +206,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 +238,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 +285,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 +317,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 +347,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 +376,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 +388,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 +400,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 +460,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..3250794d3 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -100,6 +100,31 @@                #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>            <tagNode name="ipv6-address-group">              <properties>                <help>Firewall ipv6-address-group</help> @@ -599,7 +624,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 +633,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 +642,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/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 2a5137dbf..079864122 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -95,6 +95,7 @@      </constraint>    </properties>  </leafNode> +#include <include/firewall/rule-log-level.xml.i>  <node name="connection-status">    <properties>      <help>Connection status</help> 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/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/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/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/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/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index edcf7b37f..bfad6d70f 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"> 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.xml.in b/interface-definitions/policy.xml.in index 50b7cbc84..83ae714b4 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> 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-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-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_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in index ff4c8c55f..d0d9202c1 100644 --- a/interface-definitions/service_monitoring_telegraf.xml.in +++ b/interface-definitions/service_monitoring_telegraf.xml.in @@ -20,7 +20,7 @@                  <children>                    <leafNode name="organization">                      <properties> -                      <help>Authentication organization for InfluxDB v2 [REQUIRED]</help> +                      <help>Authentication organization for InfluxDB v2</help>                        <constraint>                          <regex>[a-zA-Z][1-9a-zA-Z@_\-.]{2,50}</regex>                        </constraint> @@ -29,7 +29,7 @@                    </leafNode>                    <leafNode name="token">                      <properties> -                      <help>Authentication token for InfluxDB v2 [REQUIRED]</help> +                      <help>Authentication token for InfluxDB v2</help>                        <valueHelp>                          <format>txt</format>                          <description>Authentication token</description> @@ -42,6 +42,94 @@                    </leafNode>                  </children>                </node> +              <node name="azure-data-explorer"> +                <properties> +                  <help>Output plugin Azure Data Explorer</help> +                </properties> +                <children> +                  <node name="authentication"> +                    <properties> +                      <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-Z0-9]+</regex> +                      </constraint> +                      <constraintErrorMessage>Database is limited to alphanumerical characters and can contain hyphen and underscores</constraintErrorMessage> +                    </properties> +                  </leafNode> +                  <leafNode name="group-metrics"> +                    <properties> +                      <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>Table name</description> +                      </valueHelp> +                      <constraint> +                        <regex>[-_a-zA-Z0-9]+</regex> +                      </constraint> +                      <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> @@ -193,7 +281,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,19 +294,7 @@                    </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/monitoring/url.xml.i>                #include <include/port-number.xml.i>                <leafNode name="port">                  <defaultValue>8086</defaultValue> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index bb11e9cd0..40dac23ca 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> 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_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in index 9a75bc27d..42f5bba9f 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> 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-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/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..d884d6ce6 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -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> @@ -669,7 +669,7 @@                #include <include/generic-disable-node.xml.i>                <node name="authentication">                  <properties> -                  <help>Authentication [REQUIRED]</help> +                  <help>Authentication</help>                  </properties>                  <children>                    <leafNode name="mode"> @@ -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> @@ -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> @@ -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_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 25a573887..3604b41c8 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -58,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> @@ -87,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/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/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..7ecce4f78 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -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/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-ip.xml.in b/op-mode-definitions/show-ip.xml.in index 30c08f8e8..d21c38ccc 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -4,7 +4,7 @@      <children>        <node name="ip">          <properties> -          <help>Show IPv4 routing information</help> +          <help>Show IPv4 networking information</help>          </properties>          <children>            <node name="external"> @@ -15,9 +15,29 @@            </node>            <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/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 04fd44173..31fe8b5e3 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 @@ -16,9 +16,70 @@  import re +from vyos.util import call  from vyos.util import cmd  from vyos.util import dict_search_args + +# 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      results = cmd(f'sudo nft -a list chain {table} {chain}').split("\n") @@ -118,6 +179,14 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):                          operator = '!='                          group_name = group_name[1:]                      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} @{group_name}')                  elif 'network_group' in group:                      group_name = group['network_group']                      operator = '' @@ -148,7 +217,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_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': '<'} 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/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..fd91fc9bf 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: diff --git a/python/vyos/template.py b/python/vyos/template.py index 132f5ddde..ee82f8f8f 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') diff --git a/python/vyos/util.py b/python/vyos/util.py index de55e108b..0d62fbfe9 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -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/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/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/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/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index b8f944575..b1fd663d2 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -57,10 +57,21 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_commit()      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,15 +79,20 @@ 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'] +            ['ether saddr { 00:01:02:03:04:05 }', 'return'], +            ['set smoketest_domain'], +            ['elements = { 192.0.2.5, 192.0.2.8,'], +            ['192.0.2.10, 192.0.2.11 }'], +            ['ip saddr @smoketest_domain', 'return']          ]          nftables_output = cmd('sudo nft list table ip filter') @@ -89,20 +105,29 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):                      break              self.assertTrue(matched, msg=search) +        self.cli_delete(['system', 'static-host-mapping']) +        self.cli_commit() +      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', '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', '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(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) @@ -110,10 +135,10 @@ 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','return'], +            ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'log prefix "[smoketest-2-R]" level err', 'reject'],              ['tcp dport { 22 }', 'limit rate 5/minute', 'return'], -            ['smoketest default-action', 'drop'] +            ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop']          ]          nftables_output = cmd('sudo nft list table ip filter') @@ -128,9 +153,14 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):      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,9 +171,9 @@ 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') 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 e8c6ff19b..f175d7df7 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -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'          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,25 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):                              'peer' : peer,                          },                      }, +                    '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' : { @@ -917,10 +947,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']: @@ -929,8 +967,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']: @@ -1057,12 +1103,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) @@ -1075,8 +1133,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"]}' 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_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_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/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/firewall.py b/src/conf_mode/firewall.py index 6924bf555..fbe0a3a13 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,7 +26,13 @@ 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 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 process_named_running @@ -408,6 +414,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(group) +                nft_add_set_elements(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') 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/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.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_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 b6371d09f..b247ce2ab 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -105,7 +105,7 @@ def apply(nhrp):              remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle)      action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' -    run(f'systemctl {action} opennhrp') +    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_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 102a87318..daf75d740 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -103,11 +103,28 @@ def get_config(config=None):         '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 + ['prometheus-client']):          del monitoring['prometheus_client'] +    if not conf.exists(base + ['azure-data-explorer']): +        del monitoring['azure_data_explorer'] +      return monitoring  def verify(monitoring): @@ -124,6 +141,24 @@ def verify(monitoring):          if 'url' not in monitoring:              raise ConfigError(f'Monitoring "url" is mandatory!') +    # 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_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/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/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py new file mode 100755 index 000000000..e8501cfc6 --- /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(set_name, elements) +        time.sleep(timeout) 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/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/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/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/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/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..691f674b2 --- /dev/null +++ b/src/system/vyos-event-handler.py @@ -0,0 +1,160 @@ +#!/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 select +import re +import json +from os import getpid, environ +from pathlib import Path +from signal import signal, SIGTERM, SIGINT +from systemd import journal +from sys import exit +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 = 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/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 | 
