diff options
66 files changed, 2446 insertions, 200 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/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 index ab7b14d6b..df98e74d6 100644 --- a/data/templates/frr/daemons.frr.tmpl +++ b/data/templates/frr/daemons.frr.tmpl @@ -8,7 +8,7 @@ isisd=yes  pimd=no  ldpd=yes  nhrpd=no -eigrpd=no +eigrpd=yes  babeld=no  sharpd=no  pbrd=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/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/firewall.xml.in b/interface-definitions/firewall.xml.in index ff8d92a24..d0218579c 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,5}?(\/.*)?</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> 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/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/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/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-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_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/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/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 91564440d..d342ac192 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -4,14 +4,34 @@      <children>        <node name="ip">          <properties> -          <help>Show IPv4 routing information</help> +          <help>Show IPv4 networking information</help>          </properties>          <children>            <node name="neighbors">              <properties> -              <help>Show IPv4 Neighbor Discovery (ND) information</help> +              <help>Show IPv4 neighbor (ARP) table</help>              </properties>              <command>${vyos_op_scripts_dir}/show_neigh.py --family inet</command> +            <children> +              <tagNode name="interface"> +                <properties> +                  <help>Show IPv4 neighbor table for specified interface</help> +                  <completionHelp> +                    <script>${vyos_completion_dir}/list_interfaces.py -b</script> +                  </completionHelp> +                </properties> +                <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --interface "$5"</command> +              </tagNode> +              <tagNode name="state"> +                <properties> +                  <help>Show IPv4 neighbors with specified state</help> +                  <completionHelp> +                    <list>reachable stale failed permanent</list> +                  </completionHelp> +                </properties> +                <command>${vyos_op_scripts_dir}/show_neigh.py --family inet --state "$5"</command> +              </tagNode> +            </children>            </node>          </children>        </node> diff --git a/op-mode-definitions/show-ipv6.xml.in b/op-mode-definitions/show-ipv6.xml.in index a59c8df0c..66bc2485a 100644 --- a/op-mode-definitions/show-ipv6.xml.in +++ b/op-mode-definitions/show-ipv6.xml.in @@ -4,7 +4,7 @@      <children>        <node name="ipv6">          <properties> -          <help>Show IPv6 routing information</help> +          <help>Show IPv6 networking information</help>          </properties>          <children>            <node name="access-list"> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 04fd44173..b962c4f18 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 = '' 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/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/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-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/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index b8f944575..79d22e361 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -57,10 +57,19 @@ 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_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 +77,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,6 +103,9 @@ 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', 'rule', '1', 'action', 'accept']) 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/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 6924bf555..335098bf1 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,26 @@ 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 = [] +                for address in group_config['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/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_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_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/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/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/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/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 | 
