diff options
96 files changed, 2695 insertions, 455 deletions
@@ -43,6 +43,9 @@ interface_definitions: $(config_xml_obj) # T2773 - EIGRP support for VRF rm -rf $(TMPL_DIR)/vrf/name/node.tag/protocols/eigrp + # T4518, T4470 Load-balancing wan + rm -rf $(TMPL_DIR)/load-balancing + # 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/op-mode-standardized.json b/data/op-mode-standardized.json new file mode 100644 index 000000000..b5e9308c5 --- /dev/null +++ b/data/op-mode-standardized.json @@ -0,0 +1,12 @@ +[ +"bridge.py", +"conntrack.py", +"container.py", +"cpu.py", +"memory.py", +"nat.py", +"neighbor.py", +"route.py", +"version.py", +"vrf.py" +] diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 003b138b2..ca19506f2 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -3,8 +3,10 @@ {% macro nptv6_rule(rule,config, chain) %} {% set comment = '' %} {% set base_log = '' %} -{% set src_prefix = 'ip6 saddr ' ~ config.source.prefix if config.source.prefix is vyos_defined %} -{% set dest_address = 'ip6 daddr ' ~ config.destination.address if config.destination.address is vyos_defined %} +{% set dst_prefix = 'ip6 daddr ' ~ config.destination.prefix.replace('!','!= ') if config.destination.prefix is vyos_defined %} +{% set src_prefix = 'ip6 saddr ' ~ config.source.prefix.replace('!','!= ') if config.source.prefix is vyos_defined %} +{% set source_address = 'ip6 saddr ' ~ config.source.address.replace('!','!= ') if config.source.address is vyos_defined %} +{% set dest_address = 'ip6 daddr ' ~ config.destination.address.replace('!','!= ') if config.destination.address is vyos_defined %} {% if chain is vyos_defined('PREROUTING') %} {% set comment = 'DST-NAT66-' ~ rule %} {% set base_log = '[NAT66-DST-' ~ rule %} @@ -52,6 +54,12 @@ {% if src_prefix is vyos_defined %} {% set output = output ~ ' ' ~ src_prefix %} {% endif %} +{% if dst_prefix is vyos_defined %} +{% set output = output ~ ' ' ~ dst_prefix %} +{% endif %} +{% if source_address is vyos_defined %} +{% set output = output ~ ' ' ~ source_address %} +{% endif %} {% if dest_address is vyos_defined %} {% set output = output ~ ' ' ~ dest_address %} {% endif %} diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2 index c482002fa..b9f77a257 100644 --- a/data/templates/ids/fastnetmon.j2 +++ b/data/templates/ids/fastnetmon.j2 @@ -1,21 +1,25 @@ # enable this option if you want to send logs to local syslog facility +logging:logging_level = debug logging:local_syslog_logging = on # list of all your networks in CIDR format -networks_list_path = /etc/networks_list +networks_list_path = /run/fastnetmon/networks_list # list networks in CIDR format which will be not monitored for attacks -white_list_path = /etc/networks_whitelist +white_list_path = /run/fastnetmon/excluded_networks_list # Enable/Disable any actions in case of attack enable_ban = on +enable_ban_ipv6 = on ## How many packets will be collected from attack traffic ban_details_records_count = 500 ## How long (in seconds) we should keep an IP in blocked state ## If you set 0 here it completely disables unban capability -ban_time = 1900 +{% if ban_time is vyos_defined %} +ban_time = {{ ban_time }} +{% endif %} # Check if the attack is still active, before triggering an unban callback with this option # If the attack is still active, check each run of the unban watchdog diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2 new file mode 100644 index 000000000..c88a1c527 --- /dev/null +++ b/data/templates/ids/fastnetmon_excluded_networks_list.j2 @@ -0,0 +1,5 @@ +{% if excluded_network is vyos_defined %} +{% for net in excluded_network %} +{{ net }} +{% endfor %} +{% endif %} diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2 index 1c81180be..5f1b3ba4d 100644 --- a/data/templates/ids/fastnetmon_networks_list.j2 +++ b/data/templates/ids/fastnetmon_networks_list.j2 @@ -1,6 +1,4 @@ -{% if network is vyos_defined(var_type=str) %} -{{ network }} -{% else %} +{% if network is vyos_defined() %} {% for net in network %} {{ net }} {% endfor %} diff --git a/data/templates/macsec/wpa_supplicant.conf.j2 b/data/templates/macsec/wpa_supplicant.conf.j2 index 0ac7cb860..1f7ba16f4 100644 --- a/data/templates/macsec/wpa_supplicant.conf.j2 +++ b/data/templates/macsec/wpa_supplicant.conf.j2 @@ -47,6 +47,12 @@ network={ # 1: Integrity only macsec_integ_only={{ '0' if security.encrypt is vyos_defined else '1' }} + # macsec_csindex: IEEE 802.1X/MACsec cipher suite + # 0 = GCM-AES-128 + # 1 = GCM-AES-256 +{# security.cipher is a mandatory key #} + macsec_csindex={{ '1' if security.cipher is vyos_defined('gcm-aes-256') else '0' }} + {% if security.encrypt is vyos_defined %} # mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode # This allows to configure MACsec with a pre-shared key using a (CAK,CKN) pair. @@ -83,5 +89,9 @@ network={ # 1..2^32-1: number of packets that could be misordered macsec_replay_window={{ security.replay_window }} {% endif %} + + # macsec_port: IEEE 802.1X/MACsec port - Port component of the SCI + # Range: 1-65534 (default: 1) + macsec_port=1 } diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index ed15b32f0..a464795ad 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -50,6 +50,8 @@ interface {{ iface }} { AdvValidLifetime {{ prefix_options.valid_lifetime }}; AdvOnLink {{ 'off' if prefix_options.no_on_link_flag is vyos_defined else 'on' }}; AdvPreferredLifetime {{ prefix_options.preferred_lifetime }}; + DeprecatePrefix {{ 'on' if prefix_options.deprecate_prefix is vyos_defined else 'off' }}; + DecrementLifetimes {{ 'on' if prefix_options.decrement_lifetime is vyos_defined else 'off' }}; }; {% endfor %} {% endif %} diff --git a/data/templates/squid/squid.conf.j2 b/data/templates/squid/squid.conf.j2 index a0fdeb20e..5781c883f 100644 --- a/data/templates/squid/squid.conf.j2 +++ b/data/templates/squid/squid.conf.j2 @@ -2,6 +2,11 @@ acl net src all acl SSL_ports port 443 +{% if ssl_safe_ports is vyos_defined %} +{% for port in ssl_safe_ports %} +acl SSL_ports port {{ port }} +{% endfor %} +{% endif %} acl Safe_ports port 80 # http acl Safe_ports port 21 # ftp acl Safe_ports port 443 # https @@ -13,6 +18,11 @@ acl Safe_ports port 280 # http-mgmt acl Safe_ports port 488 # gss-http acl Safe_ports port 591 # filemaker acl Safe_ports port 777 # multiling http +{% if safe_ports is vyos_defined %} +{% for port in safe_ports %} +acl Safe_ports port {{ port }} +{% endfor %} +{% endif %} acl CONNECT method CONNECT {% if authentication is vyos_defined %} diff --git a/data/templates/ssh/override.conf.j2 b/data/templates/ssh/override.conf.j2 index e4d6f51cb..4454ad1b8 100644 --- a/data/templates/ssh/override.conf.j2 +++ b/data/templates/ssh/override.conf.j2 @@ -5,8 +5,9 @@ After=vyos-router.service ConditionPathExists={{ config_file }} [Service] +EnvironmentFile= ExecStart= -ExecStart={{ vrf_command }}/usr/sbin/sshd -f {{ config_file }} -D $SSHD_OPTS +ExecStart={{ vrf_command }}/usr/sbin/sshd -f {{ config_file }} Restart=always RestartPreventExitStatus= RestartSec=10 diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index da935bd4c..6879b6e4f 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -88,11 +88,17 @@ fi # Remove unwanted daemon files from /etc # conntackd +# pmacct +# fastnetmon +# ntp DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd - /etc/default/pmacctd /etc/pmacct" -for file in $DELETE; do - if [ -f ${file} ]; then - rm -f ${file} + /etc/default/pmacctd /etc/pmacct + /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf + /etc/ntp.conf /etc/default/ssh + /etc/powerdns /etc/default/pdns-recursor" +for tmp in $DELETE; do + if [ -e ${tmp} ]; then + rm -rf ${tmp} fi done diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index d2c393036..d096c4ff1 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -107,6 +107,19 @@ <valueless/> </properties> </leafNode> + <node name="gql"> + <properties> + <help>GraphQL support</help> + </properties> + <children> + <leafNode name="introspection"> + <properties> + <help>Schema introspection</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> <node name="cors"> <properties> <help>Set CORS options</help> diff --git a/interface-definitions/include/interface/dhcp-options.xml.i b/interface-definitions/include/interface/dhcp-options.xml.i index 914b60503..2ed5fd403 100644 --- a/interface-definitions/include/interface/dhcp-options.xml.i +++ b/interface-definitions/include/interface/dhcp-options.xml.i @@ -14,6 +14,12 @@ <help>Override system host-name sent to DHCP server</help> </properties> </leafNode> + <leafNode name="mtu"> + <properties> + <help>Use MTU value from DHCP server - ignore interface setting</help> + <valueless/> + </properties> + </leafNode> <leafNode name="vendor-class-id"> <properties> <help>Identify the vendor client type to the DHCP server</help> diff --git a/interface-definitions/include/ipv4-address-prefix-range.xml.i b/interface-definitions/include/ipv4-address-prefix-range.xml.i new file mode 100644 index 000000000..aadc6aaec --- /dev/null +++ b/interface-definitions/include/ipv4-address-prefix-range.xml.i @@ -0,0 +1,39 @@ +<!-- include start from ipv4-address-prefix-range.xml.i --> +<leafNode name="address"> + <properties> + <help>IP address, subnet, or range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range to match</description> + </valueHelp> + <valueHelp> + <format>!ipv4</format> + <description>Match everything except the specified address</description> + </valueHelp> + <valueHelp> + <format>!ipv4net</format> + <description>Match everything except the specified prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv4range</format> + <description>Match everything except the specified range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + <validator name="ipv4-range"/> + <validator name="ipv4-address-exclude"/> + <validator name="ipv4-prefix-exclude"/> + <validator name="ipv4-range-exclude"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/port-port-range.xml.i b/interface-definitions/include/port-port-range.xml.i new file mode 100644 index 000000000..ce550f549 --- /dev/null +++ b/interface-definitions/include/port-port-range.xml.i @@ -0,0 +1,26 @@ +<!-- include start from port-port-range.xml.i --> +<leafNode name="port"> + <properties> + <help>Port number</help> + <valueHelp> + <format>txt</format> + <description>Named port (any name in /etc/services, e.g., http)</description> + </valueHelp> + <valueHelp> + <format>u32:1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <valueHelp> + <format>start-end</format> + <description>Numbered port range (e.g. 1001-1005)</description> + </valueHelp> + <valueHelp> + <format/> + <description>\n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005'</description> + </valueHelp> + <constraint> + <validator name="port-multi"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/load-balancing-wan.xml.in b/interface-definitions/load-balancing-wan.xml.in new file mode 100644 index 000000000..c2b6316ae --- /dev/null +++ b/interface-definitions/load-balancing-wan.xml.in @@ -0,0 +1,395 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="load-balancing"> + <properties> + <help>Configure load-balancing</help> + </properties> + <children> + <node name="wan" owner="${vyos_conf_scripts_dir}/load-balancing-wan.py"> + <properties> + <help>Configure Wide Area Network (WAN) load-balancing</help> + </properties> + <children> + <leafNode name="disable-source-nat"> + <properties> + <help>Disable source NAT rules from being configured for WAN load balancing</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="enable-local-traffic"> + <properties> + <help>Enable WAN load balancing for locally sourced traffic</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="flush-connections"> + <properties> + <help>Flush connection tracking tables on connection state change</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="hook"> + <properties> + <help>Script to be executed on interface status change</help> + <valueHelp> + <format>txt</format> + <description>Script in /config/scripts</description> + </valueHelp> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <tagNode name="interface-health"> + <properties> + <help>Interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="failure-count"> + <properties> + <help>Failure count</help> + <valueHelp> + <format>u32:1-10</format> + <description>Failure count</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + <leafNode name="nexthop"> + <properties> + <help>Outbound interface nexthop address. Can be 'DHCP or IPv4 address' [REQUIRED]</help> + <completionHelp> + <list>dhcp</list> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>Nexthop IP address</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Set the nexthop via DHCP</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <regex>(dhcp)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="success-count"> + <properties> + <help>Success count</help> + <valueHelp> + <format>u32:1-10</format> + <description>Success count</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + <tagNode name="test"> + <properties> + <help>Rule number</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Rule number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + <children> + <leafNode name="resp-time"> + <properties> + <help>Ping response time (seconds)</help> + <valueHelp> + <format>u32:1-30</format> + <description>Response time (seconds)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-30"/> + </constraint> + </properties> + </leafNode> + <leafNode name="target"> + <properties> + <help>Health target address</help> + <valueHelp> + <format>ipv4</format> + <description>Health target address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="test-script"> + <properties> + <help>Path to user-defined script</help> + <valueHelp> + <format>txt</format> + <description>Script in /config/scripts</description> + </valueHelp> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ttl-limit"> + <properties> + <help>TTL limit (hop count)</help> + <valueHelp> + <format>u32:1-254</format> + <description>Number of hops</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-254"/> + </constraint> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>WLB test type</help> + <completionHelp> + <list>ping ttl user-defined</list> + </completionHelp> + <valueHelp> + <format>ping</format> + <description>Test with ICMP echo response</description> + </valueHelp> + <valueHelp> + <format>ttl</format> + <description>Test with UDP TTL expired response</description> + </valueHelp> + <valueHelp> + <format>user-defined</format> + <description>User-defined test script</description> + </valueHelp> + <constraint> + <regex>(ping|ttl|user-defined)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="rule"> + <properties> + <help>Rule number (1-9999)</help> + <valueHelp> + <format>u32:1-9999</format> + <description>Rule number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-9999"/> + </constraint> + </properties> + <children> + <leafNode name="description"> + <properties> + <help>Description for this rule</help> + <valueHelp> + <format>txt</format> + <description>Description for this rule</description> + </valueHelp> + </properties> + </leafNode> + <node name="destination"> + <properties> + <help>Destination</help> + </properties> + <children> + #include <include/ipv4-address-prefix-range.xml.i> + #include <include/port-port-range.xml.i> + </children> + </node> + <leafNode name="exclude"> + <properties> + <help>Exclude packets matching this rule from WAN load balance</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="failover"> + <properties> + <help>Enable failover for packets matching this rule from WAN load balance</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="inbound-interface"> + <properties> + <help>Inbound interface name (e.g., "eth0") [REQUIRED]</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Interface name [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="weight"> + <properties> + <help>Load-balance weight</help> + <valueHelp> + <format>u32:1-255</format> + <description>Interface weight</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>Weight must be between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + <node name="limit"> + <properties> + <help>Enable packet limit for this rule</help> + </properties> + <children> + <leafNode name="burst"> + <properties> + <help>Burst limit for matching packets</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Burst limit for matching packets</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="period"> + <properties> + <help>Time window for rate calculation</help> + <completionHelp> + <list>hour minute second</list> + </completionHelp> + <valueHelp> + <format>hour</format> + <description>hour</description> + </valueHelp> + <valueHelp> + <format>minute</format> + <description>minute</description> + </valueHelp> + <valueHelp> + <format>second</format> + <description>second</description> + </valueHelp> + <constraint> + <regex>(hour|minute|second)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="rate"> + <properties> + <help>Number of packets used for rate limit</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Number of packets used for rate limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="threshold"> + <properties> + <help>Threshold behavior for limit</help> + <completionHelp> + <list>above below</list> + </completionHelp> + <valueHelp> + <format>above</format> + <description>Above limit</description> + </valueHelp> + <valueHelp> + <format>below</format> + <description>Below limit</description> + </valueHelp> + <constraint> + <regex>(above|below)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="per-packet-balancing"> + <properties> + <help>Option to match traffic per-packet instead of the default, per-flow</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>${vyos_completion_dir}/list_protocols.sh</script> + <list>all tcp_udp</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>u32:0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format><protocol></format> + <description>IP protocol name</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol name</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>Source information</help> + </properties> + <children> + #include <include/ipv4-address-prefix-range.xml.i> + #include <include/port-port-range.xml.i> + </children> + </node> + </children> + </tagNode> + <node name="sticky-connections"> + <properties> + <help>Configure sticky connections</help> + </properties> + <children> + <leafNode name="inbound"> + <properties> + <help>Enable sticky incoming WAN connections</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index b47f653c6..ac3198f45 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -49,6 +49,30 @@ </completionHelp> </properties> </leafNode> + <node name="destination"> + <properties> + <help>IPv6 destination prefix options</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>IPv6 prefix to be translated</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + <validator name="ipv6-prefix-exclude"/> + </constraint> + </properties> + </leafNode> + </children> + </node> <node name="source"> <properties> <help>IPv6 source prefix options</help> @@ -61,8 +85,13 @@ <format>ipv6net</format> <description>IPv6 prefix</description> </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> <constraint> <validator name="ipv6-prefix"/> + <validator name="ipv6-prefix-exclude"/> </constraint> </properties> </leafNode> @@ -164,9 +193,53 @@ <format>ipv6net</format> <description>IPv6 prefix</description> </valueHelp> + <valueHelp> + <format>!ipv6</format> + <description>Match everything except the specified IPv6 address</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + <validator name="ipv6-address-exclude"/> + <validator name="ipv6-prefix-exclude"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="source"> + <properties> + <help>IPv6 source prefix options</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address or prefix to be translated</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv6</format> + <description>Match everything except the specified IPv6 address</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> <constraint> <validator name="ipv6-address"/> <validator name="ipv6-prefix"/> + <validator name="ipv6-address-exclude"/> + <validator name="ipv6-prefix-exclude"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index 15c2beefa..cc1de609d 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -639,7 +639,7 @@ </leafNode> <leafNode name="prefix-len"> <properties> - <help>IP prefix-length to match (cannot be used for BGP routes)</help> + <help>IP prefix-length to match (can be used for kernel routes only)</help> <valueHelp> <format>u32:0-32</format> <description>Prefix length</description> @@ -809,7 +809,7 @@ </leafNode> <leafNode name="prefix-len"> <properties> - <help>IPv6 prefix-length to match (cannot be used for BGP routes)</help> + <help>IPv6 prefix-length to match (can be used for kernel routes only)</help> <valueHelp> <format>u32:0-128</format> <description>Prefix length</description> diff --git a/interface-definitions/service-ids-ddos-protection.xml.in b/interface-definitions/service-ids-ddos-protection.xml.in index 5e65d3106..86fc4dffa 100644 --- a/interface-definitions/service-ids-ddos-protection.xml.in +++ b/interface-definitions/service-ids-ddos-protection.xml.in @@ -18,6 +18,19 @@ <help>Path to fastnetmon alert script</help> </properties> </leafNode> + <leafNode name="ban-time"> + <properties> + <help>How long we should keep an IP in blocked state</help> + <valueHelp> + <format>u32:1-4294967294</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + </properties> + <defaultValue>1900</defaultValue> + </leafNode> <leafNode name="direction"> <properties> <help>Direction for processing traffic</help> @@ -30,6 +43,24 @@ <multi/> </properties> </leafNode> + <leafNode name="excluded-network"> + <properties> + <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix(es) to exclude</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix(es) to exclude</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> <leafNode name="listen-interface"> <properties> <help>Listen interface for mirroring traffic</help> @@ -55,13 +86,18 @@ </node> <leafNode name="network"> <properties> - <help>Define monitoring networks</help> + <help>Specify IPv4 and IPv6 networks which belong to you</help> <valueHelp> <format>ipv4net</format> - <description>Processed network</description> + <description>Your IPv4 prefix(es)</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Your IPv6 prefix(es)</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> </constraint> <multi/> </properties> diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service-router-advert.xml.in index 258b7b749..87ec512d6 100644 --- a/interface-definitions/service-router-advert.xml.in +++ b/interface-definitions/service-router-advert.xml.in @@ -249,6 +249,18 @@ <valueless/> </properties> </leafNode> + <leafNode name="deprecate-prefix"> + <properties> + <help>Upon shutdown, this option will deprecate the prefix by announcing it in the shutdown RA</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="decrement-lifetime"> + <properties> + <help>Lifetime is decremented by the number of seconds since the last RA - use in conjunction with a DHCPv6-PD prefix</help> + <valueless/> + </properties> + </leafNode> <leafNode name="preferred-lifetime"> <properties> <help>Time in seconds that the prefix will remain preferred</help> diff --git a/interface-definitions/service-webproxy.xml.in b/interface-definitions/service-webproxy.xml.in index e4609b699..a315aa2ef 100644 --- a/interface-definitions/service-webproxy.xml.in +++ b/interface-definitions/service-webproxy.xml.in @@ -8,6 +8,32 @@ <priority>500</priority> </properties> <children> + <leafNode name="safe-ports"> + <properties> + <help>Safe port ACL</help> + <valueHelp> + <format>u32:1-1024</format> + <description>Port number. Ports included by default: 21,70,80,210,280,443,488,591,777,873,1025-65535</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-20 --range 22-69 --range 71-79 --range 81-209 --range 211-279 --range 281-442 --range 444-487 --range 489-590 --range 592-776 --range 778-872 --range 874-1024"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="ssl-safe-ports"> + <properties> + <help>SSL safe port</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Port number. Ports included by default: 443</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-442 --range 444-65535"/> + </constraint> + <multi/> + </properties> + </leafNode> <leafNode name="append-domain"> <properties> <help>Default domain name</help> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index fa66402dc..a7048e5ed 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -100,13 +100,13 @@ <properties> <help>Show containers</help> </properties> - <command>sudo podman ps --all</command> + <command>sudo ${vyos_op_scripts_dir}/container.py show_container</command> <children> <leafNode name="image"> <properties> <help>Show container image</help> </properties> - <command>sudo podman image ls</command> + <command>sudo ${vyos_op_scripts_dir}/container.py show_image</command> </leafNode> <tagNode name="log"> <properties> @@ -121,7 +121,7 @@ <properties> <help>Show available container networks</help> </properties> - <command>sudo podman network ls</command> + <command>sudo ${vyos_op_scripts_dir}/container.py show_network</command> </leafNode> </children> </node> @@ -149,7 +149,7 @@ <path>container name</path> </completionHelp> </properties> - <command>sudo podman restart "$3"</command> + <command>sudo ${vyos_op_scripts_dir}/container.py restart name="$3"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index baf60efbd..5dea5b91b 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -74,7 +74,7 @@ <properties> <help>Show DNS forwarding statistics</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py show_forwarding_statistics</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/generate-macsec-key.xml.in b/op-mode-definitions/generate-macsec-key.xml.in index 40d2b9061..d8e514c16 100644 --- a/op-mode-definitions/generate-macsec-key.xml.in +++ b/op-mode-definitions/generate-macsec-key.xml.in @@ -7,17 +7,37 @@ <help>Generate MACsec Key</help> </properties> <children> - <node name="mka-cak"> + <node name="mka"> <properties> - <help>Generate MACsec connectivity association key (CAK)</help> + <help>MACsec Key Agreement (MKA) protocol</help> </properties> - <command>/usr/bin/hexdump -n 16 -e '4/4 "%08x" 1 "\n"' /dev/random</command> - </node> - <node name="mka-ckn"> - <properties> - <help>Generate MACsec connectivity association name (CKN)</help> - </properties> - <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command> + <children> + <node name="cak"> + <properties> + <help>Generate MACsec connectivity association key (CAK)</help> + </properties> + <children> + <leafNode name="gcm-aes-128"> + <properties> + <help>Generate random key for GCM-AES-128 encryption - 128bit</help> + </properties> + <command>/usr/bin/hexdump -n 16 -e '4/4 "%08x" 1 "\n"' /dev/random</command> + </leafNode> + <leafNode name="gcm-aes-256"> + <properties> + <help>Generate random key for GCM-AES-256 encryption - 256bit</help> + </properties> + <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command> + </leafNode> + </children> + </node> + <node name="ckn"> + <properties> + <help>Generate MACsec connectivity association name (CKN) - 256bit</help> + </properties> + <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command> + </node> + </children> </node> </children> </node> diff --git a/op-mode-definitions/ipoe-server.xml.in b/op-mode-definitions/ipoe-server.xml.in index 89cefa08d..3aee303dc 100644 --- a/op-mode-definitions/ipoe-server.xml.in +++ b/op-mode-definitions/ipoe-server.xml.in @@ -4,12 +4,12 @@ <children> <node name="ipoe-server"> <properties> - <help>Clear IPoE server sessions or process</help> + <help>IPoE (Internet Protocol over Ethernet) server</help> </properties> <children> <node name="session"> <properties> - <help>Clear IPoE server session</help> + <help>Clear IPoE (Internet Protocol over Ethernet) server session</help> </properties> <children> <tagNode name="username"> @@ -49,7 +49,7 @@ <children> <node name="ipoe-server"> <properties> - <help>Show IPoE server status</help> + <help>Show IPoE (Internet Protocol over Ethernet) server status</help> </properties> <children> <leafNode name="sessions"> @@ -72,7 +72,7 @@ <children> <leafNode name="ipoe-server"> <properties> - <help>Restart IPoE server process</help> + <help>Restart IPoE (Internet Protocol over Ethernet) server process</help> </properties> <command>${vyos_op_scripts_dir}/ipoe-control.py --action="restart"</command> </leafNode> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index f5e0ede59..8a02e1f08 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -14,6 +14,19 @@ </properties> <command>grc journalctl --no-hostname --follow --boot</command> </node> + <node name="ids"> + <properties> + <help>Monitor log for Intrusion Detection System</help> + </properties> + <children> + <leafNode name="ddos-protection"> + <properties> + <help>Monitor last lines of DDOS protection</help> + </properties> + <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command> + </leafNode> + </children> + </node> <node name="dhcp"> <properties> <help>Monitor last lines of Dynamic Host Control Protocol (DHCP)</help> @@ -111,7 +124,7 @@ </node> <node name="protocol"> <properties> - <help>Monitor log for Routing Protocols</help> + <help>Monitor log for Routing Protocol</help> </properties> <children> <leafNode name="ospf"> @@ -182,6 +195,35 @@ </leafNode> </children> </node> + <node name="macsec"> + <properties> + <help>Monitor last lines of MACsec</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Monitor last lines of specific MACsec interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + </completionHelp> + </properties> + <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@$SRC.service"</command> + </tagNode> + </children> + </node> + <leafNode name="snmp"> + <properties> + <help>Monitor last lines of Simple Network Monitoring Protocol (SNMP)</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit snmpd.service</command> + </leafNode> + <leafNode name="ssh"> + <properties> + <help>Monitor last lines of Secure Shell (SSH)</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit ssh.service</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index 084e2e7e3..7148c1128 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -16,13 +16,13 @@ <properties> <help>Show configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat_rules.py --source</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source</command> </node> <node name="statistics"> <properties> <help>Show statistics for configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command> + <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command> </node> <node name="translations"> <properties> @@ -58,7 +58,7 @@ <properties> <help>Show configured destination NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat_rules.py --destination</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination</command> </node> <node name="statistics"> <properties> diff --git a/op-mode-definitions/nhrp.xml.in b/op-mode-definitions/nhrp.xml.in index 89508e2be..c10b111a7 100644 --- a/op-mode-definitions/nhrp.xml.in +++ b/op-mode-definitions/nhrp.xml.in @@ -43,7 +43,7 @@ <children> <node name="nhrp"> <properties> - <help>Show NHRP info</help> + <help>Show NHRP (Next Hop Resolution Protocol) information</help> </properties> <children> <leafNode name="interface"> diff --git a/op-mode-definitions/pptp-server.xml.in b/op-mode-definitions/pptp-server.xml.in index 59be68611..f6f8104d8 100644 --- a/op-mode-definitions/pptp-server.xml.in +++ b/op-mode-definitions/pptp-server.xml.in @@ -4,7 +4,7 @@ <children> <node name="pptp-server"> <properties> - <help>Show PPTP server information</help> + <help>Show PPTP (Point-to-Point Tunneling Protocol) server information</help> </properties> <children> <leafNode name="sessions"> diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in index 0f8d3064d..dd2a28931 100644 --- a/op-mode-definitions/show-bridge.xml.in +++ b/op-mode-definitions/show-bridge.xml.in @@ -11,7 +11,7 @@ <properties> <help>View the VLAN filter settings of the bridge</help> </properties> - <command>bridge -c vlan show</command> + <command>${vyos_op_scripts_dir}/bridge.py show_vlan</command> </leafNode> </children> </node> @@ -19,7 +19,7 @@ <properties> <help>Show bridging information</help> </properties> - <command>bridge -c link show</command> + <command>${vyos_op_scripts_dir}/bridge.py show</command> </leafNode> <tagNode name="bridge"> <properties> @@ -34,13 +34,13 @@ <properties> <help>Displays the multicast group database for the bridge</help> </properties> - <command>bridge -c mdb show dev $3</command> + <command>${vyos_op_scripts_dir}/bridge.py show_mdb --interface=$3</command> </leafNode> <leafNode name="fdb"> <properties> <help>Show the forwarding database of the bridge</help> </properties> - <command>bridge -c fdb show br $3</command> + <command>${vyos_op_scripts_dir}/bridge.py show_fdb --interface=$3</command> </leafNode> </children> </tagNode> diff --git a/op-mode-definitions/show-conntrack.xml.in b/op-mode-definitions/show-conntrack.xml.in index 792623d7d..8d921e6a5 100644 --- a/op-mode-definitions/show-conntrack.xml.in +++ b/op-mode-definitions/show-conntrack.xml.in @@ -16,7 +16,13 @@ <properties> <help>Show conntrack entries for IPv4 protocol</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_conntrack.py</command> + <command>sudo ${vyos_op_scripts_dir}/conntrack.py show --family inet</command> + </node> + <node name="ipv6"> + <properties> + <help>Show conntrack entries for IPv6 protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/conntrack.py show --family inet6</command> </node> </children> </node> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 76879e5d6..24a1b5f3e 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -32,6 +32,19 @@ </properties> <command>journalctl --no-hostname --boot --unit conntrackd.service</command> </leafNode> + <node name="ids"> + <properties> + <help>Show log for for Intrusion Detection System</help> + </properties> + <children> + <leafNode name="ddos-protection"> + <properties> + <help>Show log for DDOS protection</help> + </properties> + <command>journalctl --no-hostname --boot --unit fastnetmon.service</command> + </leafNode> + </children> + </node> <node name="dhcp"> <properties> <help>Show log for Dynamic Host Control Protocol (DHCP)</help> @@ -207,6 +220,23 @@ </properties> <command>journalctl --no-hostname --boot --unit opennhrp.service</command> </leafNode> + <node name="macsec"> + <properties> + <help>Show log for MACsec</help> + </properties> + <command>journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show MACsec log on specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + </completionHelp> + </properties> + <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@$SRC.service"</command> + </tagNode> + </children> + </node> <node name="openvpn"> <properties> <help>Show log for OpenVPN</help> @@ -243,7 +273,7 @@ </node> <node name="protocol"> <properties> - <help>Show log for Routing Protocols</help> + <help>Show log for Routing Protocol</help> </properties> <children> <leafNode name="ospf"> @@ -320,6 +350,12 @@ </properties> <command>journalctl --no-hostname --boot --unit snmpd.service</command> </leafNode> + <leafNode name="ssh"> + <properties> + <help>Show log for Secure Shell (SSH)</help> + </properties> + <command>journalctl --no-hostname --boot --unit ssh.service</command> + </leafNode> <tagNode name="tail"> <properties> <help>Show last n changes to messages</help> diff --git a/op-mode-definitions/show-rpki.xml.in b/op-mode-definitions/show-rpki.xml.in index f593e4803..c1902ccec 100644 --- a/op-mode-definitions/show-rpki.xml.in +++ b/op-mode-definitions/show-rpki.xml.in @@ -4,7 +4,7 @@ <children> <node name="rpki"> <properties> - <help>Show RPKI information</help> + <help>Show RPKI (Resource Public Key Infrastructure) information</help> </properties> <children> <leafNode name="cache-connection"> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 6f05d0c12..60ed28b6f 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -53,7 +53,7 @@ <properties> <help>Show CPU information</help> </properties> - <command>${vyos_op_scripts_dir}/show_cpu.py</command> + <command>${vyos_op_scripts_dir}/cpu.py show</command> </leafNode> <leafNode name="kernel-messages"> <properties> diff --git a/op-mode-definitions/show-vrf.xml.in b/op-mode-definitions/show-vrf.xml.in index 9c38c30fe..9728eb1fa 100644 --- a/op-mode-definitions/show-vrf.xml.in +++ b/op-mode-definitions/show-vrf.xml.in @@ -4,9 +4,9 @@ <children> <node name="vrf"> <properties> - <help>Show VRF information</help> + <help>Show VRF (Virtual Routing and Forwarding) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_vrf.py -e</command> + <command>${vyos_op_scripts_dir}/vrf.py show</command> </node> <tagNode name="vrf"> <properties> @@ -15,7 +15,7 @@ <path>vrf name</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> + <command>${vyos_op_scripts_dir}/vrf.py show --name="$3"</command> <children> <leafNode name="processes"> <properties> diff --git a/op-mode-definitions/show-zebra.xml.in b/op-mode-definitions/show-zebra.xml.in index b0ad37f49..69991a1d5 100644 --- a/op-mode-definitions/show-zebra.xml.in +++ b/op-mode-definitions/show-zebra.xml.in @@ -4,7 +4,7 @@ <children> <node name="zebra"> <properties> - <help>Zebra routing information</help> + <help>Show Zebra routing information</help> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index 3d997c143..a98cf8ff2 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -19,16 +19,16 @@ <properties> <help>Reset a specific tunnel for given peer</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="$6"</command> + <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="$6"</command> </tagNode> <node name="vti"> <properties> <help>Reset the VTI tunnel for given peer</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="vti"</command> + <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="vti"</command> </node> </children> - <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="all"</command> + <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="all"</command> </tagNode> <tagNode name="ipsec-profile"> <properties> @@ -76,6 +76,9 @@ <tagNode name="peer"> <properties> <help>Show debugging information for a peer</help> + <completionHelp> + <path>vpn ipsec site-to-site peer</path> + </completionHelp> </properties> <children> <tagNode name="tunnel"> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index a61666afc..8f822a97d 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -222,24 +222,10 @@ def is_member(conf, interface, intftype=None): for intf in conf.list_nodes(base): member = base + [intf, 'member', 'interface', interface] if conf.exists(member): - member_type = Section.section(interface) - # Check if it's a VLAN (QinQ) interface - interface = interface.split('.') - if len(interface) == 3: - if conf.exists(['interfaces', member_type, interface[0], 'vif-s', interface[1], 'vif-c', interface[2]]): - tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], - key_mangling=('-', '_'), get_first_key=True) - ret_val.update({intf : tmp}) - elif len(interface) == 2: - if conf.exists(['interfaces', member_type, interface[0], 'vif', interface[1]]): - tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], - key_mangling=('-', '_'), get_first_key=True) - ret_val.update({intf : tmp}) - else: - if conf.exists(['interfaces', member_type, interface[0]]): - tmp = conf.get_config_dict(['interfaces', member_type, interface[0]], - key_mangling=('-', '_'), get_first_key=True) - ret_val.update({intf : tmp}) + tmp = conf.get_config_dict(member, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + ret_val.update({intf : tmp}) return ret_val @@ -317,8 +303,8 @@ def is_source_interface(conf, interface, intftype=None): for it in intftype: base = ['interfaces', it] for intf in conf.list_nodes(base): - lower_intf = base + [intf, 'source-interface'] - if conf.exists(lower_intf) and interface in conf.return_values(lower_intf): + src_intf = base + [intf, 'source-interface'] + if conf.exists(src_intf) and interface in conf.return_values(src_intf): ret_val = intf break diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 137eb9f79..2ab3cb408 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -243,10 +243,10 @@ def verify_address(config): of a bridge or bond. """ if {'is_bridge_member', 'address'} <= set(config): - raise ConfigError( - 'Cannot assign address to interface "{ifname}" as it is a ' - 'member of bridge "{is_bridge_member}"!'.format(**config)) - + interface = config['ifname'] + bridge_name = next(iter(config['is_bridge_member'])) + raise ConfigError(f'Cannot assign address to interface "{interface}" ' + f'as it is a member of bridge "{bridge_name}"!') def verify_bridge_delete(config): """ @@ -256,9 +256,9 @@ def verify_bridge_delete(config): """ if 'is_bridge_member' in config: interface = config['ifname'] - for bridge in config['is_bridge_member']: - raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' - f'is a member of bridge "{bridge}"!') + bridge_name = next(iter(config['is_bridge_member'])) + raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' + f'is a member of bridge "{bridge_name}"!') def verify_interface_exists(ifname): """ @@ -284,15 +284,16 @@ def verify_source_interface(config): raise ConfigError('Specified source-interface {source_interface} does ' 'not exist'.format(**config)) + src_ifname = config['source_interface'] if 'source_interface_is_bridge_member' in config: - raise ConfigError('Invalid source-interface {source_interface}. Interface ' - 'is already a member of bridge ' - '{source_interface_is_bridge_member}'.format(**config)) + bridge_name = next(iter(config['source_interface_is_bridge_member'])) + raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface ' + f'is already a member of bridge "{bridge_name}"!') if 'source_interface_is_bond_member' in config: - raise ConfigError('Invalid source-interface {source_interface}. Interface ' - 'is already a member of bond ' - '{source_interface_is_bond_member}'.format(**config)) + bond_name = next(iter(config['source_interface_is_bond_member'])) + raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface ' + f'is already a member of bond "{bond_name}"!') def verify_dhcpv6(config): """ diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index fcb6a7fbc..09ae73eac 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -18,6 +18,7 @@ import os directories = { "data": "/usr/share/vyos/", "conf_mode": "/usr/libexec/vyos/conf_mode", + "op_mode": "/usr/libexec/vyos/op_mode", "config": "/opt/vyatta/etc/config", "current": "/opt/vyatta/etc/config-migrate/current", "migrate": "/opt/vyatta/etc/config-migrate/migrate", @@ -49,6 +50,7 @@ api_data = { 'socket' : False, 'strict' : False, 'gql' : False, + 'introspection' : False, 'debug' : False, 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] } diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index e4db69c1f..758967fbc 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -187,6 +187,11 @@ class BridgeIf(Interface): """ self.set_interface('vlan_filter', state) + # VLAN of bridge parent interface is always 1 + # VLAN 1 is the default VLAN for all unlabeled packets + cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self' + self._cmd(cmd) + def set_multicast_querier(self, enable): """ Sets whether the bridge actively runs a multicast querier or not. When a @@ -293,30 +298,6 @@ class BridgeIf(Interface): vlan_filter = '1' if 'enable_vlan' in config else '0' self.set_vlan_filter(vlan_filter) - ifname = config['ifname'] - if int(vlan_filter): - add_vlan = [] - cur_vlan_ids = get_vlan_ids(ifname) - - tmp = dict_search('vif', config) - if tmp: - for vif, vif_config in tmp.items(): - add_vlan.append(vif) - - # Remove redundant VLANs from the system - for vlan in list_diff(cur_vlan_ids, add_vlan): - cmd = f'bridge vlan del dev {ifname} vid {vlan} self' - self._cmd(cmd) - - for vlan in add_vlan: - cmd = f'bridge vlan add dev {ifname} vid {vlan} self' - self._cmd(cmd) - - # VLAN of bridge parent interface is always 1 - # VLAN 1 is the default VLAN for all unlabeled packets - cmd = f'bridge vlan add dev {ifname} vid 1 pvid untagged self' - self._cmd(cmd) - tmp = dict_search('member.interface', config) if tmp: for interface, interface_config in tmp.items(): @@ -346,15 +327,13 @@ class BridgeIf(Interface): # set bridge port path cost if 'cost' in interface_config: - value = interface_config.get('cost') - lower.set_path_cost(value) + lower.set_path_cost(interface_config['cost']) # set bridge port path priority if 'priority' in interface_config: - value = interface_config.get('priority') - lower.set_path_priority(value) + lower.set_path_priority(interface_config['priority']) - if int(vlan_filter): + if 'enable_vlan' in config: add_vlan = [] native_vlan_id = None allowed_vlan_ids= [] @@ -384,6 +363,7 @@ class BridgeIf(Interface): for vlan in allowed_vlan_ids: cmd = f'bridge vlan add dev {interface} vid {vlan} master' self._cmd(cmd) + # Setting native VLAN to system if native_vlan_id: cmd = f'bridge vlan add dev {interface} vid {native_vlan_id} pvid untagged master' diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 555494f80..c50ead89f 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1535,7 +1535,7 @@ class Interface(Control): # before mangling any IPv6 option. If MTU is less then 1280 IPv6 will be # automatically disabled by the kernel. Also MTU must be increased before # configuring any IPv6 address on the interface. - if 'mtu' in config: + if 'mtu' in config and dict_search('dhcp_options.mtu', config) == None: self.set_mtu(config.get('mtu')) # Configure MSS value for IPv6 TCP connections diff --git a/python/vyos/util.py b/python/vyos/util.py index bee5d7aec..b86b1949c 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -164,6 +164,27 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, return decoded +def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=STDOUT, decode='utf-8'): + """ + A wrapper around popen, which returns the return code + of a command and stdout + + % rc_cmd('uname') + (0, 'Linux') + % rc_cmd('ip link show dev eth99') + (1, 'Device "eth99" does not exist.') + """ + out, code = popen( + command, flag, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + decode=decode, + ) + return code, out + + def call(command, flag='', shell=None, input=None, timeout=None, env=None, stdout=PIPE, stderr=PIPE, decode='utf-8'): """ diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index d4515b8db..b008596dc 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -27,6 +27,7 @@ import copy import functools from lxml import etree as ET +from textwrap import fill # Defaults validator_dir = "/opt/vyatta/libexec/validators" @@ -123,13 +124,15 @@ def make_node_def(props, command): node_def = "" if "help" in props: - node_def += "help: {0}\n".format(props["help"]) + help = props["help"] + help = fill(help, width=64, subsequent_indent='\t\t\t') + node_def += f'help: {help}\n' if "comp_help" in props: - node_def += "allowed: {0}\n".format(props["comp_help"]) + node_def += f'allowed: {props["comp_help"]}\n' if command is not None: - node_def += "run: {0}\n".format(command.text) + node_def += f'run: {command.text}\n' if debug: - print("The contents of the node.def file:\n", node_def) + print('Contents of the node.def file:\n', node_def) return node_def diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 664dc48bc..8f711af20 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -224,6 +224,51 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): super().test_vif_8021q_mtu_limits() def test_bridge_vlan_filter(self): + def _verify_members() -> None: + # check member interfaces are added on the bridge + for interface in self._interfaces: + bridge_members = [] + for tmp in glob(f'/sys/class/net/{interface}/lower_*'): + bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + + # We can not use assertListEqual() b/c the position of the interface + # names within the list is not fixed + self.assertEqual(len(self._members), len(bridge_members)) + for member in self._members: + self.assertIn(member, bridge_members) + + def _check_vlan_filter() -> None: + for interface in self._interfaces: + tmp = cmd(f'bridge -j vlan show dev {interface}') + tmp = json.loads(tmp) + self.assertIsNotNone(tmp) + + for interface_status in tmp: + ifname = interface_status['ifname'] + for interface in self._members: + vlan_success = 0; + if interface == ifname: + vlans_status = interface_status['vlans'] + for vlan_status in vlans_status: + vlan_id = vlan_status['vlan'] + flag_num = 0 + if 'flags' in vlan_status: + flags = vlan_status['flags'] + for flag in flags: + flag_num = flag_num +1 + if vlan_id == 2: + if flag_num == 0: + vlan_success = vlan_success + 1 + else: + for id in range(4,10): + if vlan_id == id: + if flag_num == 0: + vlan_success = vlan_success + 1 + if vlan_id >= 101: + if flag_num == 2: + vlan_success = vlan_success + 1 + self.assertGreaterEqual(vlan_success, 7) + vif_vlan = 2 # Add member interface to bridge and set VLAN filter for interface in self._interfaces: @@ -247,61 +292,50 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # commit config self.cli_commit() - # Detect the vlan filter function + # Verify correct setting of VLAN filter function for interface in self._interfaces: tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering') self.assertEqual(tmp, '1') - # Execute the program to obtain status information - json_data = cmd('bridge -j vlan show', shell=True) - vlan_filter_status = None - vlan_filter_status = json.loads(json_data) - - if vlan_filter_status is not None: - for interface_status in vlan_filter_status: - ifname = interface_status['ifname'] - for interface in self._members: - vlan_success = 0; - if interface == ifname: - vlans_status = interface_status['vlans'] - for vlan_status in vlans_status: - vlan_id = vlan_status['vlan'] - flag_num = 0 - if 'flags' in vlan_status: - flags = vlan_status['flags'] - for flag in flags: - flag_num = flag_num +1 - if vlan_id == 2: - if flag_num == 0: - vlan_success = vlan_success + 1 - else: - for id in range(4,10): - if vlan_id == id: - if flag_num == 0: - vlan_success = vlan_success + 1 - if vlan_id >= 101: - if flag_num == 2: - vlan_success = vlan_success + 1 - if vlan_success >= 7: - self.assertTrue(True) - else: - self.assertTrue(False) + # Execute the program to obtain status information and verify proper + # VLAN filter setup + _check_vlan_filter() - else: - self.assertTrue(False) + # check member interfaces are added on the bridge + _verify_members() + + # change member interface description to trigger config update, + # VLANs must still exist (T4565) + for interface in self._interfaces: + for member in self._members: + self.cli_set(['interfaces', Section.section(member), member, 'description', f'foo {member}']) + + # commit config + self.cli_commit() # check member interfaces are added on the bridge + _verify_members() + + # Execute the program to obtain status information and verify proper + # VLAN filter setup + _check_vlan_filter() + + # delete all members + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'member']) + + # commit config + self.cli_commit() + + # verify member interfaces are no longer assigned on the bridge for interface in self._interfaces: bridge_members = [] for tmp in glob(f'/sys/class/net/{interface}/lower_*'): bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + self.assertNotEqual(len(self._members), len(bridge_members)) for member in self._members: - self.assertIn(member, bridge_members) - - # delete all members - for interface in self._interfaces: - self.cli_delete(self._base_path + [interface, 'member']) + self.assertNotIn(member, bridge_members) def test_bridge_vif_members(self): diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index e5e5a558e..f141cc6d3 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -28,6 +28,8 @@ from vyos.util import read_file from vyos.util import get_interface_config from vyos.util import process_named_running +PROCESS_NAME = 'wpa_supplicant' + def get_config_value(interface, key): tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) @@ -55,6 +57,10 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(MACsecInterfaceTest, cls).setUpClass() + def tearDown(self): + super().tearDown() + self.assertFalse(process_named_running(PROCESS_NAME)) + def test_macsec_encryption(self): # MACsec can be operating in authentication and encryption mode - both # using different mandatory settings, lets test encryption as the basic @@ -96,28 +102,29 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() tmp = get_config_value(src_interface, 'macsec_integ_only') - self.assertTrue("0" in tmp) + self.assertIn("0", tmp) tmp = get_config_value(src_interface, 'mka_cak') - self.assertTrue(mak_cak in tmp) + self.assertIn(mak_cak, tmp) tmp = get_config_value(src_interface, 'mka_ckn') - self.assertTrue(mak_ckn in tmp) + self.assertIn(mak_ckn, tmp) # check that the default priority of 255 is programmed tmp = get_config_value(src_interface, 'mka_priority') - self.assertTrue("255" in tmp) + self.assertIn("255", tmp) tmp = get_config_value(src_interface, 'macsec_replay_window') - self.assertTrue(replay_window in tmp) + self.assertIn(replay_window, tmp) tmp = read_file(f'/sys/class/net/{interface}/mtu') self.assertEqual(tmp, '1460') - # Check for running process - self.assertTrue(process_named_running('wpa_supplicant')) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) def test_macsec_gcm_aes_128(self): + src_interface = 'eth0' interface = 'macsec1' cipher = 'gcm-aes-128' self.cli_set(self._base_path + [interface]) @@ -125,7 +132,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # check validate() - source interface is mandatory with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(self._base_path + [interface, 'source-interface', 'eth0']) + self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): @@ -138,7 +145,15 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) + # check that we use the new macsec_csindex option (T4537) + tmp = get_config_value(src_interface, 'macsec_csindex') + self.assertIn("0", tmp) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_macsec_gcm_aes_256(self): + src_interface = 'eth0' interface = 'macsec4' cipher = 'gcm-aes-256' self.cli_set(self._base_path + [interface]) @@ -146,7 +161,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # check validate() - source interface is mandatory with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(self._base_path + [interface, 'source-interface', 'eth0']) + self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): @@ -158,6 +173,13 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) + # check that we use the new macsec_csindex option (T4537) + tmp = get_config_value(src_interface, 'macsec_csindex') + self.assertIn("1", tmp) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_macsec_source_interface(self): # Ensure source-interface can bot be part of any other bond or bridge @@ -186,6 +208,9 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() self.assertIn(interface, interfaces()) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index e0b5cccec..f69832cb4 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -921,5 +921,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {peer_group} peer-group', frrconfig) self.assertIn(f' neighbor {peer_group} remote-as {remote_asn}', frrconfig) + def test_bgp_15_local_as_ebgp(self): + # https://phabricator.vyos.net/T4560 + # local-as allowed only for ebgp peers + + neighbor = '192.0.2.99' + remote_asn = '500' + local_asn = '400' + + self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN]) + self.cli_set(base_path + ['neighbor', neighbor, 'local-as', local_asn]) + + # check validate() - local-as allowed only for ebgp peers + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig) + self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 71fb3e177..72c1d4e43 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -138,5 +138,62 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Must get HTTP code 401 on missing key (Unauthorized) self.assertEqual(r.status_code, 401) + # GraphQL auth test: a missing key will return status code 400, as + # 'key' is a non-nullable field in the schema; an incorrect key is + # caught by the resolver, and returns success 'False', so one must + # check the return value. + + self.cli_set(base_path + ['api', 'gql']) + self.cli_commit() + + gql_url = f'https://{address}/graphql' + + query_valid_key = f""" + {{ + SystemStatus (data: {{key: "{key}"}}) {{ + success + errors + data {{ + result + }} + }} + }} + """ + + r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_valid_key}) + success = r.json()['data']['SystemStatus']['success'] + self.assertTrue(success) + + query_invalid_key = """ + { + SystemStatus (data: {key: "invalid"}) { + success + errors + data { + result + } + } + } + """ + + r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_invalid_key}) + success = r.json()['data']['SystemStatus']['success'] + self.assertFalse(success) + + query_no_key = """ + { + SystemStatus (data: {}) { + success + errors + data { + result + } + } + } + """ + + r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_no_key}) + self.assertEqual(r.status_code, 400) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ids.py b/smoketest/scripts/cli/test_service_ids.py index 18f1b8ec5..d471eeaed 100755 --- a/smoketest/scripts/cli/test_service_ids.py +++ b/smoketest/scripts/cli/test_service_ids.py @@ -24,7 +24,9 @@ from vyos.util import process_named_running from vyos.util import read_file PROCESS_NAME = 'fastnetmon' -FASTNETMON_CONF = '/etc/fastnetmon.conf' +FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf' +NETWORKS_CONF = '/run/fastnetmon/networks_list' +EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list' base_path = ['service', 'ids', 'ddos-protection'] class TestServiceIDS(VyOSUnitTestSHIM.TestCase): @@ -48,7 +50,8 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) def test_fastnetmon(self): - networks = ['10.0.0.0/24', '10.5.5.0/24'] + networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64'] + excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128'] interfaces = ['eth0', 'eth1'] fps = '3500' mbps = '300' @@ -61,6 +64,12 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase): for tmp in networks: self.cli_set(base_path + ['network', tmp]) + # optional excluded-network! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for tmp in excluded_networks: + self.cli_set(base_path + ['excluded-network', tmp]) + # Required interface(s)! with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -86,9 +95,22 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'threshold_mbps = {mbps}', config) self.assertIn(f'ban_for_pps = on', config) self.assertIn(f'threshold_pps = {pps}', config) + # default + self.assertIn(f'enable_ban = on', config) + self.assertIn(f'enable_ban_ipv6 = on', config) + self.assertIn(f'ban_time = 1900', config) tmp = ','.join(interfaces) self.assertIn(f'interfaces = {tmp}', config) + + network_config = read_file(NETWORKS_CONF) + for tmp in networks: + self.assertIn(f'{tmp}', network_config) + + excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF) + for tmp in excluded_networks: + self.assertIn(f'{tmp}', excluded_network_config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 1168c05cd..873be7df0 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -23,11 +23,13 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import read_file from vyos.util import process_named_running +PROCESS_NAME = 'radvd' RADVD_CONF = '/run/radvd/radvd.conf' interface = 'eth1' base_path = ['service', 'router-advert', 'interface', interface] address_base = ['interfaces', 'ethernet', interface, 'address'] +prefix = '::/64' def get_config_value(key): tmp = read_file(RADVD_CONF) @@ -35,18 +37,36 @@ def get_config_value(key): return tmp[0].split()[0].replace(';','') class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(address_base + ['2001:db8::1/64']) + + @classmethod + def setUpClass(cls): + super(TestServiceRADVD, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, ['service', 'router-advert']) + + cls.cli_set(cls, address_base + ['2001:db8::1/64']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, address_base) + super(TestServiceRADVD, cls).tearDownClass() def tearDown(self): - self.cli_delete(address_base) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) self.cli_commit() + # Check for no longer running process + self.assertFalse(process_named_running(PROCESS_NAME)) + def test_common(self): - self.cli_set(base_path + ['prefix', '::/64', 'no-on-link-flag']) - self.cli_set(base_path + ['prefix', '::/64', 'no-autonomous-flag']) - self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) + self.cli_set(base_path + ['prefix', prefix, 'no-on-link-flag']) + self.cli_set(base_path + ['prefix', prefix, 'no-autonomous-flag']) + self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity']) self.cli_set(base_path + ['other-config-flag']) # commit changes @@ -57,7 +77,7 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, interface) tmp = get_config_value('prefix') - self.assertEqual(tmp, '::/64') + self.assertEqual(tmp, prefix) tmp = get_config_value('AdvOtherConfigFlag') self.assertEqual(tmp, 'on') @@ -88,15 +108,19 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('AdvOnLink') self.assertEqual(tmp, 'off') - # Check for running process - self.assertTrue(process_named_running('radvd')) + tmp = get_config_value('DeprecatePrefix') + self.assertEqual(tmp, 'off') + + tmp = get_config_value('DecrementLifetimes') + self.assertEqual(tmp, 'off') + def test_dns(self): nameserver = ['2001:db8::1', '2001:db8::2'] dnssl = ['vyos.net', 'vyos.io'] ns_lifetime = '599' - self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) + self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity']) self.cli_set(base_path + ['other-config-flag']) for ns in nameserver: @@ -127,5 +151,21 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): self.assertIn(tmp, config) + def test_deprecate_prefix(self): + self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity']) + self.cli_set(base_path + ['prefix', prefix, 'deprecate-prefix']) + self.cli_set(base_path + ['prefix', prefix, 'decrement-lifetime']) + + # commit changes + self.cli_commit() + + config = read_file(RADVD_CONF) + + tmp = get_config_value('DeprecatePrefix') + self.assertEqual(tmp, 'on') + + tmp = get_config_value('DecrementLifetimes') + self.assertEqual(tmp, 'on') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index 772d6ab16..fb9b46a06 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.py @@ -87,6 +87,8 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): max_obj_size = '8192' block_mine = ['application/pdf', 'application/x-sh'] body_max_size = '4096' + safe_port = '88' + ssl_safe_port = '8443' self.cli_set(base_path + ['listen-address', listen_ip]) self.cli_set(base_path + ['append-domain', domain]) @@ -104,6 +106,9 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['reply-body-max-size', body_max_size]) + self.cli_set(base_path + ['safe-ports', safe_port]) + self.cli_set(base_path + ['ssl-safe-ports', ssl_safe_port]) + # commit changes self.cli_commit() @@ -122,6 +127,9 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'reply_body_max_size {body_max_size} KB', config) + self.assertIn(f'acl Safe_ports port {safe_port}', config) + self.assertIn(f'acl SSL_ports port {ssl_safe_port}', config) + # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 7750c1247..7e16235c1 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -192,6 +192,11 @@ def verify(flow_config): raise ConfigError("All sFlow servers must use the same IP protocol") else: sflow_collector_ipver = ip_address(server).version + + # check if vrf is defined for Sflow + sflow_vrf = None + if 'vrf' in flow_config: + sflow_vrf = flow_config['vrf'] # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa for server in flow_config['sflow']['server']: @@ -203,12 +208,12 @@ def verify(flow_config): if 'agent_address' in flow_config['sflow']: tmp = flow_config['sflow']['agent_address'] - if not is_addr_assigned(tmp): + if not is_addr_assigned(tmp, sflow_vrf): raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!') # Check if configured netflow source-address exist in the system if 'source_address' in flow_config['sflow']: - if not is_addr_assigned(flow_config['sflow']['source_address']): + if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf): tmp = flow_config['sflow']['source_address'] raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!') diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 4a7906c17..04113fc09 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -66,14 +66,10 @@ def get_config(config=None): if conf.exists('debug'): http_api['debug'] = True - # this node is not available by CLI by default, and is reserved for - # the graphql tools. One can enable it for testing, with the warning - # that this will open an unauthenticated server. To do so - # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql - # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def - # and configure; editing the config alone is insufficient. if conf.exists('gql'): http_api['gql'] = True + if conf.exists('gql introspection'): + http_api['introspection'] = True if conf.exists('socket'): http_api['socket'] = True diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index cd0d9003b..b961408db 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -61,7 +61,7 @@ def get_config(config=None): else: bridge.update({'member' : {'interface_remove' : tmp }}) - if dict_search('member.interface', bridge): + if dict_search('member.interface', bridge) != None: # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: # RuntimeError: dictionary changed size during iteration for interface in list(bridge['member']['interface']): @@ -103,6 +103,14 @@ def get_config(config=None): if 'enable_vlan' in bridge and tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) + # delete empty dictionary keys - no need to run code paths if nothing is there to do + if 'member' in bridge: + if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0: + del bridge['member']['interface'] + + if len(bridge['member']) == 0: + del bridge['member'] + return bridge def verify(bridge): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 03a010086..870049a88 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -22,6 +22,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -56,7 +57,7 @@ def get_config(config=None): # Check if interface has been removed if 'deleted' in macsec: - source_interface = conf.return_effective_value(['source-interface']) + source_interface = conf.return_effective_value(base + [ifname, 'source-interface']) macsec.update({'source_interface': source_interface}) if is_node_changed(conf, base + [ifname, 'security']): @@ -65,6 +66,10 @@ def get_config(config=None): if is_node_changed(conf, base + [ifname, 'source_interface']): macsec.update({'shutdown_required': {}}) + if 'source_interface' in macsec: + tmp = is_source_interface(conf, macsec['source_interface'], 'macsec') + if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp}) + return macsec @@ -87,6 +92,22 @@ def verify(macsec): if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') + cak_len = len(dict_search('security.mka.cak', macsec)) + + if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32: + # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit + raise ConfigError('gcm-aes-128 requires a 128bit long key!') + + elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64: + # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit + raise ConfigError('gcm-aes-128 requires a 256bit long key!') + + if 'is_source_interface' in macsec: + tmp = macsec['is_source_interface'] + src_ifname = macsec['source_interface'] + raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ + f'belongs to interface "{tmp}"!') + if 'source_interface' in macsec: # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad # and 802.1q) - we need to check the underlaying MTU if our configured diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index bf0f6840d..af2d0588d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -118,6 +118,11 @@ def verify(vxlan): # in use. vxlan_overhead += 20 + # If source_address is not used - check IPv6 'remote' list + elif 'remote' in vxlan: + if any(is_ipv6(a) for a in vxlan['remote']): + vxlan_overhead += 20 + lower_mtu = Interface(vxlan['source_interface']).get_mtu() if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead): raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\ diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index e275ace84..97b3a6396 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -76,7 +76,7 @@ def get_config(config=None): # We need to know the amount of other WWAN interfaces as ModemManager needs # to be started or stopped. conf.set_level(base) - _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), + wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py new file mode 100755 index 000000000..11840249f --- /dev/null +++ b/src/conf_mode/load-balancing-wan.py @@ -0,0 +1,65 @@ +#!/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 vyos.config import Config +from vyos.configdict import node_changed +from vyos.util import call +from vyos import ConfigError +from pprint import pprint +from vyos import airbag +airbag.enable() + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, get_first_key=True, + no_tag_node_value_mangle=True) + + pprint(lb) + return lb + +def verify(lb): + return None + + +def generate(lb): + if not lb: + return None + + return None + + +def apply(lb): + + 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_bgp.py b/src/conf_mode/protocols_bgp.py index e85438c00..0f8f1af42 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -213,6 +213,12 @@ def verify(bgp): if 'source_interface' in peer_config['interface']: raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') + # Local-AS allowed only for EBGP peers + if 'local_as' in peer_config: + remote_as = verify_remote_as(peer_config, bgp) + if remote_as == bgp['local_as']: + raise ConfigError(f'local-as configured for "{peer}", allowed only for eBGP peers!') + for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec', 'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec', 'l2vpn_evpn']: diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index ae7e582ec..c58f8db9a 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,14 +19,17 @@ import os from sys import exit from vyos.config import Config -from vyos import ConfigError -from vyos.util import call +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() -config_file = r'/etc/fastnetmon.conf' -networks_list = r'/etc/networks_list' +config_file = r'/run/fastnetmon/fastnetmon.conf' +networks_list = r'/run/fastnetmon/networks_list' +excluded_networks_list = r'/run/fastnetmon/excluded_networks_list' def get_config(config=None): if config: @@ -34,50 +37,55 @@ def get_config(config=None): else: conf = Config() base = ['service', 'ids', 'ddos-protection'] + if not conf.exists(base): + return None + fastnetmon = 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) + fastnetmon = dict_merge(default_values, fastnetmon) + return fastnetmon def verify(fastnetmon): if not fastnetmon: return None - if not "mode" in fastnetmon: - raise ConfigError('ddos-protection mode is mandatory!') - - if not "network" in fastnetmon: - raise ConfigError('Required define network!') + if 'mode' not in fastnetmon: + raise ConfigError('Specify operating mode!') - if not "listen_interface" in fastnetmon: - raise ConfigError('Define listen-interface is mandatory!') + if 'listen_interface' not in fastnetmon: + raise ConfigError('Specify interface(s) for traffic capture') - if "alert_script" in fastnetmon: - if os.path.isfile(fastnetmon["alert_script"]): + if 'alert_script' in fastnetmon: + if os.path.isfile(fastnetmon['alert_script']): # Check script permissions - if not os.access(fastnetmon["alert_script"], os.X_OK): - raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"])) + if not os.access(fastnetmon['alert_script'], os.X_OK): + raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script'])) else: - raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) + raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon)) def generate(fastnetmon): if not fastnetmon: - if os.path.isfile(config_file): - os.unlink(config_file) - if os.path.isfile(networks_list): - os.unlink(networks_list) + for file in [config_file, networks_list]: + if os.path.isfile(file): + os.unlink(file) - return + return None render(config_file, 'ids/fastnetmon.j2', fastnetmon) render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) - + render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon) return None def apply(fastnetmon): + systemd_service = 'fastnetmon.service' if not fastnetmon: # Stop fastnetmon service if removed - call('systemctl stop fastnetmon.service') + call(f'systemctl stop {systemd_service}') else: - call('systemctl restart fastnetmon.service') + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index ff7caaa84..1b8377a4a 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -90,8 +90,8 @@ def verify(rtradv): if preferred_lifetime == 'infinity': preferred_lifetime = 4294967295 - if not (int(valid_lifetime) > int(preferred_lifetime)): - raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if not (int(valid_lifetime) >= int(preferred_lifetime)): + raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime') if 'name_server_lifetime' in interface_config: # man page states: diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index f7487ee5f..8274e6564 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -14,16 +14,17 @@ # 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 pprint import pprint import os import re import sys import vici +from json import loads from vyos.util import cmd from vyos.util import process_named_running -NHRP_CONFIG="/run/opennhrp/opennhrp.conf" +NHRP_CONFIG = "/run/opennhrp/opennhrp.conf" + def parse_type_ipsec(interface): with open(NHRP_CONFIG, 'r') as f: @@ -35,6 +36,50 @@ def parse_type_ipsec(interface): return m[1], m[2] return None, None + +def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None: + """Add a route to a NBMA peer + + Args: + nmba_src (str): a local IP address + nbma_dst (str): a remote IP address + mtu (str): a MTU for a route + """ + # Find routes to a peer + route_get_cmd = f'sudo ip -j route get {nbma_dst} from {nbma_src}' + try: + route_info_data = loads(cmd(route_get_cmd)) + except Exception as err: + print(f'Unable to find a route to {nbma_dst}: {err}') + + # Check if an output has an expected format + if not isinstance(route_info_data, list): + print(f'Garbage returned from the "{route_get_cmd}" command: \ + {route_info_data}') + return + + # Add static routes to a peer + for route_item in route_info_data: + route_dev = route_item.get('dev') + route_dst = route_item.get('dst') + route_gateway = route_item.get('gateway') + # Prepare a command to add a route + route_add_cmd = 'sudo ip route add' + if route_dst: + route_add_cmd = f'{route_add_cmd} {route_dst}' + if route_gateway: + route_add_cmd = f'{route_add_cmd} via {route_gateway}' + if route_dev: + route_add_cmd = f'{route_add_cmd} dev {route_dev}' + route_add_cmd = f'{route_add_cmd} proto 42 mtu {mtu}' + # Add a route + try: + cmd(route_add_cmd) + except Exception as err: + print(f'Unable to add a route using command "{route_add_cmd}": \ + {err}') + + def vici_initiate(conn, child_sa, src_addr, dest_addr): try: session = vici.Session() @@ -52,6 +97,7 @@ def vici_initiate(conn, child_sa, src_addr, dest_addr): except: return None + def vici_terminate(conn, child_sa, src_addr, dest_addr): try: session = vici.Session() @@ -69,25 +115,27 @@ def vici_terminate(conn, child_sa, src_addr, dest_addr): except: return None + def iface_up(interface): cmd(f'sudo ip route flush proto 42 dev {interface}') cmd(f'sudo ip neigh flush dev {interface}') + def peer_up(dmvpn_type, conn): - src_addr = os.getenv('NHRP_SRCADDR') + # src_addr = os.getenv('NHRP_SRCADDR') src_nbma = os.getenv('NHRP_SRCNBMA') - dest_addr = os.getenv('NHRP_DESTADDR') + # dest_addr = os.getenv('NHRP_DESTADDR') dest_nbma = os.getenv('NHRP_DESTNBMA') dest_mtu = os.getenv('NHRP_DESTMTU') if dest_mtu: - args = cmd(f'sudo ip route get {dest_nbma} from {src_nbma}') - cmd(f'sudo ip route add {args} proto 42 mtu {dest_mtu}') + add_peer_route(src_nbma, dest_nbma, dest_mtu) if conn and dmvpn_type == 'spoke' and process_named_running('charon'): vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma) + def peer_down(dmvpn_type, conn): src_nbma = os.getenv('NHRP_SRCNBMA') dest_nbma = os.getenv('NHRP_DESTNBMA') @@ -97,14 +145,17 @@ def peer_down(dmvpn_type, conn): cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') + def route_up(interface): dest_addr = os.getenv('NHRP_DESTADDR') dest_prefix = os.getenv('NHRP_DESTPREFIX') next_hop = os.getenv('NHRP_NEXTHOP') - cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 via {next_hop} dev {interface}') + cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \ + via {next_hop} dev {interface}') cmd('sudo ip route flush cache') + def route_down(interface): dest_addr = os.getenv('NHRP_DESTADDR') dest_prefix = os.getenv('NHRP_DESTPREFIX') @@ -112,6 +163,7 @@ def route_down(interface): cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') cmd('sudo ip route flush cache') + if __name__ == '__main__': action = sys.argv[1] interface = os.getenv('NHRP_INTERFACE') diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf new file mode 100644 index 000000000..841666070 --- /dev/null +++ b/src/etc/systemd/system/fastnetmon.service.d/override.conf @@ -0,0 +1,12 @@ +[Unit] +RequiresMountsFor=/run +ConditionPathExists=/run/fastnetmon/fastnetmon.conf +After= +After=vyos-router.service + +[Service] +Type=simple +WorkingDirectory=/run/fastnetmon +PIDFile=/run/fastnetmon.pid +ExecStart= +ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf diff --git a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf index a895e675f..5cffb7987 100644 --- a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf +++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf @@ -7,4 +7,5 @@ WorkingDirectory= WorkingDirectory=/run/wpa_supplicant PIDFile=/run/wpa_supplicant/%I.pid ExecStart= -ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -i%I +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -P/run/wpa_supplicant/%I.pid -i%I +ExecReload=/bin/kill -HUP $MAINPID diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py new file mode 100755 index 000000000..411aa06d1 --- /dev/null +++ b/src/op_mode/bridge.py @@ -0,0 +1,202 @@ +#!/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 jmespath +import json +import sys +import typing + +from sys import exit +from tabulate import tabulate + +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode + + +def _get_json_data(): + """ + Get bridge data format JSON + """ + return cmd(f'sudo bridge --json link show') + + +def _get_raw_data_summary(): + """Get interested rules + :returns dict + """ + data = _get_json_data() + data_dict = json.loads(data) + return data_dict + + +def _get_raw_data_vlan(): + """ + :returns dict + """ + json_data = cmd('sudo bridge --json --compressvlans vlan show') + data_dict = json.loads(json_data) + return data_dict + + +def _get_raw_data_fdb(bridge): + """Get MAC-address for the bridge brX + :returns list + """ + json_data = cmd(f'sudo bridge --json fdb show br {bridge}') + data_dict = json.loads(json_data) + return data_dict + + +def _get_raw_data_mdb(bridge): + """Get MAC-address multicast gorup for the bridge brX + :return list + """ + json_data = cmd(f'bridge --json mdb show br {bridge}') + data_dict = json.loads(json_data) + return data_dict + + +def _get_bridge_members(bridge: str) -> list: + """ + Get list of interface bridge members + :param bridge: str + :default: ['n/a'] + :return: list + """ + data = _get_raw_data_summary() + members = jmespath.search(f'[?master == `{bridge}`].ifname', data) + return [member for member in members] if members else ['n/a'] + + +def _get_member_options(bridge: str): + data = _get_raw_data_summary() + options = jmespath.search(f'[?master == `{bridge}`]', data) + return options + + +def _get_formatted_output_summary(data): + data_entries = '' + bridges = set(jmespath.search('[*].master', data)) + for bridge in bridges: + member_options = _get_member_options(bridge) + member_entries = [] + for option in member_options: + interface = option.get('ifname') + ifindex = option.get('ifindex') + state = option.get('state') + mtu = option.get('mtu') + flags = ','.join(option.get('flags')).lower() + prio = option.get('priority') + member_entries.append([interface, state, mtu, flags, prio]) + member_headers = ["Member", "State", "MTU", "Flags", "Prio"] + output_members = tabulate(member_entries, member_headers, numalign="left") + output_bridge = f"""Bridge interface {bridge}: +{output_members} + +""" + data_entries += output_bridge + output = data_entries + return output + + +def _get_formatted_output_vlan(data): + data_entries = [] + for entry in data: + interface = entry.get('ifname') + vlans = entry.get('vlans') + for vlan_entry in vlans: + vlan = vlan_entry.get('vlan') + if vlan_entry.get('vlanEnd'): + vlan_end = vlan_entry.get('vlanEnd') + vlan = f'{vlan}-{vlan_end}' + flags = ', '.join(vlan_entry.get('flags')).lower() + data_entries.append([interface, vlan, flags]) + + headers = ["Interface", "Vlan", "Flags"] + output = tabulate(data_entries, headers) + return output + + +def _get_formatted_output_fdb(data): + data_entries = [] + for entry in data: + interface = entry.get('ifname') + mac = entry.get('mac') + state = entry.get('state') + flags = ','.join(entry['flags']) + data_entries.append([interface, mac, state, flags]) + + headers = ["Interface", "Mac address", "State", "Flags"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def _get_formatted_output_mdb(data): + data_entries = [] + for entry in data: + for mdb_entry in entry['mdb']: + interface = mdb_entry.get('port') + group = mdb_entry.get('grp') + state = mdb_entry.get('state') + flags = ','.join(mdb_entry.get('flags')) + data_entries.append([interface, group, state, flags]) + headers = ["Interface", "Group", "State", "Flags"] + output = tabulate(data_entries, headers) + return output + + +def show(raw: bool): + bridge_data = _get_raw_data_summary() + if raw: + return bridge_data + else: + return _get_formatted_output_summary(bridge_data) + + +def show_vlan(raw: bool): + bridge_vlan = _get_raw_data_vlan() + if raw: + return bridge_vlan + else: + return _get_formatted_output_vlan(bridge_vlan) + + +def show_fdb(raw: bool, interface: str): + fdb_data = _get_raw_data_fdb(interface) + if raw: + return fdb_data + else: + return _get_formatted_output_fdb(fdb_data) + + +def show_mdb(raw: bool, interface: str): + mdb_data = _get_raw_data_mdb(interface) + if raw: + return mdb_data + else: + return _get_formatted_output_mdb(mdb_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_conntrack.py b/src/op_mode/conntrack.py index 089a3e454..1441d110f 100755 --- a/src/op_mode/show_conntrack.py +++ b/src/op_mode/conntrack.py @@ -14,17 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import sys import xmltodict from tabulate import tabulate from vyos.util import cmd +from vyos.util import run +import vyos.opmode -def _get_raw_data(): + +def _get_xml_data(family): """ Get conntrack XML output """ - return cmd(f'sudo conntrack --dump --output xml') + return cmd(f'sudo conntrack --dump --family {family} --output xml') def _xml_to_dict(xml): @@ -32,26 +36,34 @@ def _xml_to_dict(xml): Convert XML to dictionary Return: dictionary """ - parse = xmltodict.parse(xml) + parse = xmltodict.parse(xml, attr_prefix='') # If only one conntrack entry we must change dict if 'meta' in parse['conntrack']['flow']: return dict(conntrack={'flow': [parse['conntrack']['flow']]}) return parse -def _get_formatted_output(xml): +def _get_raw_data(family): + """ + Return: dictionary + """ + xml = _get_xml_data(family) + return _xml_to_dict(xml) + + +def get_formatted_output(dict_data): """ :param xml: :return: formatted output """ data_entries = [] - dict_data = _xml_to_dict(xml) + #dict_data = _get_raw_data(family) for entry in dict_data['conntrack']['flow']: orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {} reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {} proto = {} for meta in entry['meta']: - direction = meta['@direction'] + direction = meta['direction'] if direction in ['original']: if 'layer3' in meta: orig_src = meta['layer3']['src'] @@ -61,7 +73,7 @@ def _get_formatted_output(xml): orig_sport = meta['layer4']['sport'] if meta.get('layer4').get('dport'): orig_dport = meta['layer4']['dport'] - proto = meta['layer4']['@protoname'] + proto = meta['layer4']['protoname'] if direction in ['reply']: if 'layer3' in meta: reply_src = meta['layer3']['src'] @@ -71,7 +83,7 @@ def _get_formatted_output(xml): reply_sport = meta['layer4']['sport'] if meta.get('layer4').get('dport'): reply_dport = meta['layer4']['dport'] - proto = meta['layer4']['@protoname'] + proto = meta['layer4']['protoname'] if direction == 'independent': conn_id = meta['id'] timeout = meta['timeout'] @@ -90,13 +102,20 @@ def _get_formatted_output(xml): return output -def show(raw: bool): - conntrack_data = _get_raw_data() +def show(raw: bool, family: str): + family = 'ipv6' if family == 'inet6' else 'ipv4' + conntrack_data = _get_raw_data(family) if raw: return conntrack_data else: - return _get_formatted_output(conntrack_data) + return get_formatted_output(conntrack_data) if __name__ == '__main__': - print(show(raw=False)) + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/container.py b/src/op_mode/container.py new file mode 100755 index 000000000..78d42f800 --- /dev/null +++ b/src/op_mode/container.py @@ -0,0 +1,85 @@ +#!/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 +import sys + +from sys import exit + +from vyos.util import cmd + +import vyos.opmode + + +def _get_json_data(command: str) -> list: + """ + Get container command format JSON + """ + return cmd(f'{command} --format json') + + +def _get_raw_data(command: str) -> list: + json_data = _get_json_data(command) + data = json.loads(json_data) + return data + + +def show_container(raw: bool): + command = 'sudo podman ps --all' + container_data = _get_raw_data(command) + if raw: + return container_data + else: + return cmd(command) + + +def show_image(raw: bool): + command = 'sudo podman image ls' + container_data = _get_raw_data('sudo podman image ls') + if raw: + return container_data + else: + return cmd(command) + + +def show_network(raw: bool): + command = 'sudo podman network ls' + container_data = _get_raw_data(command) + if raw: + return container_data + else: + return cmd(command) + + +def restart(name: str): + from vyos.util import rc_cmd + + rc, output = rc_cmd(f'sudo podman restart {name}') + if rc != 0: + print(output) + return None + print(f'Container name "{name}" restarted!') + return output + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py new file mode 100755 index 000000000..717652b9b --- /dev/null +++ b/src/op_mode/dns.py @@ -0,0 +1,95 @@ +#!/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 sys + +from sys import exit +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.util import cmd + +import vyos.opmode + + +def _data_to_dict(data, sep="\t") -> dict: + """ + Return dictionary from plain text + separated by tab + + cache-entries 73 + cache-hits 0 + uptime 2148 + user-msec 172 + + { + 'cache-entries': '73', + 'cache-hits': '0', + 'uptime': '2148', + 'user-msec': '172' + } + """ + dictionary = {} + mylist = [line for line in data.split('\n')] + + for line in mylist: + if sep in line: + key, value = line.split(sep) + dictionary[key] = value + return dictionary + + +def _get_raw_forwarding_statistics() -> dict: + command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all') + data = _data_to_dict(command) + data['cache-size'] = "{0:.2f}".format( int( + cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) + return data + + +def _get_formatted_forwarding_statistics(data): + cache_entries = data.get('cache-entries') + max_cache_entries = data.get('max-cache-entries') + cache_size = data.get('cache-size') + data_entries = [[cache_entries, max_cache_entries, f'{cache_size} kbytes']] + headers = ["Cache entries", "Max cache entries" , "Cache size"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def show_forwarding_statistics(raw: bool): + + config = ConfigTreeQuery() + if not config.exists('service dns forwarding'): + print("DNS forwarding is not configured") + exit(0) + + dns_data = _get_raw_forwarding_statistics() + if raw: + return dns_data + else: + return _get_formatted_forwarding_statistics(dns_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py new file mode 100755 index 000000000..432856585 --- /dev/null +++ b/src/op_mode/ipsec.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys +from vyos.util import call +import vyos.opmode + + +SWANCTL_CONF = '/etc/swanctl/swanctl.conf' + + +def get_peer_connections(peer, tunnel, return_all = False): + peer = peer.replace(':', '-') + search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' + matches = [] + with open(SWANCTL_CONF, 'r') as f: + for line in f.readlines(): + result = re.match(search, line) + if result: + suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel + if return_all or (result[2] == suffix): + matches.append(result[1]) + return matches + + +def reset_peer(peer: str, tunnel:str): + if not peer: + print('Invalid peer, aborting') + return + + conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) + + if not conns: + print('Tunnel(s) not found, aborting') + return + + result = True + for conn in conns: + try: + call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) + call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) + except TimeoutExpired as e: + print(f'Timed out while resetting {conn}') + result = False + + + print('Peer reset result: ' + ('success' if result else 'failed')) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py new file mode 100755 index 000000000..a98fc4227 --- /dev/null +++ b/src/op_mode/nat.py @@ -0,0 +1,201 @@ +#!/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 jmespath +import json +import sys + +from sys import exit +from tabulate import tabulate + +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode + + +def _get_json_data(direction): + """ + Get NAT format JSON + """ + if direction == 'source': + chain = 'POSTROUTING' + if direction == 'destination': + chain = 'PREROUTING' + return cmd(f'sudo nft --json list chain ip nat {chain}') + + +def _get_raw_data_rules(direction): + """Get interested rules + :returns dict + """ + data = _get_json_data(direction) + data_dict = json.loads(data) + rules = [] + for rule in data_dict['nftables']: + if 'rule' in rule and 'comment' in rule['rule']: + rules.append(rule) + return rules + + +def _get_formatted_output_rules(data, direction): + # Add default values before loop + sport, dport, proto = 'any', 'any', 'any' + saddr, daddr = '0.0.0.0/0', '0.0.0.0/0' + data_entries = [] + for rule in data: + if 'comment' in rule['rule']: + comment = rule.get('rule').get('comment') + rule_number = comment.split('-')[-1] + rule_number = rule_number.split(' ')[0] + if 'expr' in rule['rule']: + interface = rule.get('rule').get('expr')[0].get('match').get('right') \ + if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any' + for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)): + if 'payload' in match['left']: + if 'prefix' in match['right'] or 'set' in match['right']: + # Merge dict src/dst l3_l4 parameters + my_dict = {**match['left']['payload'], **match['right']} + proto = my_dict.get('protocol').upper() + if my_dict['field'] == 'saddr': + saddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' + elif my_dict['field'] == 'daddr': + daddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' + elif my_dict['field'] == 'sport': + # Port range or single port + if jmespath.search('set[*].range', my_dict): + sport = my_dict['set'][0]['range'] + sport = '-'.join(map(str, sport)) + else: + sport = my_dict.get('set') + sport = ','.join(map(str, sport)) + elif my_dict['field'] == 'dport': + # Port range or single port + if jmespath.search('set[*].range', my_dict): + dport = my_dict["set"][0]["range"] + dport = '-'.join(map(str, dport)) + else: + dport = my_dict.get('set') + dport = ','.join(map(str, dport)) + else: + if jmespath.search('left.payload.field', match) == 'saddr': + saddr = match.get('right') + if jmespath.search('left.payload.field', match) == 'daddr': + daddr = match.get('right') + else: + saddr = '0.0.0.0/0' + daddr = '0.0.0.0/0' + sport = 'any' + dport = 'any' + proto = 'any' + + source = f'''{saddr} +sport {sport}''' + destination = f'''{daddr} +dport {dport}''' + + if jmespath.search('left.payload.field', match) == 'protocol': + field_proto = match.get('right').upper() + + for expr in rule.get('rule').get('expr'): + if 'snat' in expr: + translation = dict_search('snat.addr', expr) + if expr['snat'] and 'port' in expr['snat']: + if jmespath.search('snat.port.range', expr): + port = dict_search('snat.port.range', expr) + port = '-'.join(map(str, port)) + else: + port = expr['snat']['port'] + translation = f'''{translation} +port {port}''' + + elif 'masquerade' in expr: + translation = 'masquerade' + if expr['masquerade'] and 'port' in expr['masquerade']: + if jmespath.search('masquerade.port.range', expr): + port = dict_search('masquerade.port.range', expr) + port = '-'.join(map(str, port)) + else: + port = expr['masquerade']['port'] + + translation = f'''{translation} +port {port}''' + elif 'dnat' in expr: + translation = dict_search('dnat.addr', expr) + if expr['dnat'] and 'port' in expr['dnat']: + if jmespath.search('dnat.port.range', expr): + port = dict_search('dnat.port.range', expr) + port = '-'.join(map(str, port)) + else: + port = expr['dnat']['port'] + translation = f'''{translation} +port {port}''' + else: + translation = 'exclude' + # Overwrite match loop 'proto' if specified filed 'protocol' exist + if 'protocol' in jmespath.search('rule.expr[*].match.left.payload.field', rule): + proto = jmespath.search('rule.expr[0].match.right', rule).upper() + + data_entries.append([rule_number, source, destination, proto, interface, translation]) + + interface_header = 'Out-Int' if direction == 'source' else 'In-Int' + headers = ["Rule", "Source", "Destination", "Proto", interface_header, "Translation"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def _get_formatted_output_statistics(data, direction): + data_entries = [] + for rule in data: + if 'comment' in rule['rule']: + comment = rule.get('rule').get('comment') + rule_number = comment.split('-')[-1] + rule_number = rule_number.split(' ')[0] + if 'expr' in rule['rule']: + interface = rule.get('rule').get('expr')[0].get('match').get('right') \ + if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any' + packets = jmespath.search('rule.expr[*].counter.packets | [0]', rule) + _bytes = jmespath.search('rule.expr[*].counter.bytes | [0]', rule) + data_entries.append([rule_number, packets, _bytes, interface]) + headers = ["Rule", "Packets", "Bytes", "Interface"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def show_rules(raw: bool, direction: str): + nat_rules = _get_raw_data_rules(direction) + if raw: + return nat_rules + else: + return _get_formatted_output_rules(nat_rules, direction) + + +def show_statistics(raw: bool, direction: str): + nat_statistics = _get_raw_data_rules(direction) + if raw: + return nat_statistics + else: + return _get_formatted_output_statistics(nat_statistics, direction) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py deleted file mode 100755 index 60a4bdd13..000000000 --- a/src/op_mode/show_nat_rules.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import jmespath -import json - -from argparse import ArgumentParser -from jinja2 import Template -from sys import exit -from vyos.util import cmd -from vyos.util import dict_search - -parser = ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") -group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") -args = parser.parse_args() - -if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip nat') - tmp = json.loads(tmp) - - format_nat_rule = '{0: <10} {1: <50} {2: <50} {3: <10}' - print(format_nat_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) - print(format_nat_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------")) - - data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp) - for idx in range(0, len(data_json)): - data = data_json[idx] - - # The following key values must exist - # When the rule JSON does not have some keys, this is not a rule we can work with - continue_rule = False - for key in ['comment', 'chain', 'expr']: - if key not in data: - continue_rule = True - continue - if continue_rule: - continue - - comment = data['comment'] - - # Check the annotation to see if the annotation format is created by VYOS - continue_rule = True - for comment_prefix in ['SRC-NAT-', 'DST-NAT-']: - if comment_prefix in comment: - continue_rule = False - if continue_rule: - continue - - rule = int(''.join(list(filter(str.isdigit, comment)))) - chain = data['chain'] - if not ((args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING')): - continue - interface = dict_search('match.right', data['expr'][0]) - srcdest = '' - srcdests = [] - tran_addr = '' - for i in range(1,len(data['expr']) ): - srcdest_json = dict_search('match.right', data['expr'][i]) - if srcdest_json: - if isinstance(srcdest_json,str): - if srcdest != '': - srcdests.append(srcdest) - srcdest = '' - srcdest = srcdest_json + ' ' - elif 'prefix' in srcdest_json: - addr_tmp = dict_search('match.right.prefix.addr', data['expr'][i]) - len_tmp = dict_search('match.right.prefix.len', data['expr'][i]) - if addr_tmp and len_tmp: - srcdest = addr_tmp + '/' + str(len_tmp) + ' ' - elif 'set' in srcdest_json: - if isinstance(srcdest_json['set'][0],int): - srcdest += 'port ' + str(srcdest_json['set'][0]) + ' ' - else: - port_range = srcdest_json['set'][0]['range'] - srcdest += 'port ' + str(port_range[0]) + '-' + str(port_range[1]) + ' ' - - tran_addr_json = dict_search('snat' if args.source else 'dnat', data['expr'][i]) - if tran_addr_json: - if isinstance(tran_addr_json['addr'],str): - tran_addr += tran_addr_json['addr'] + ' ' - elif 'prefix' in tran_addr_json['addr']: - addr_tmp = dict_search('snat.addr.prefix.addr' if args.source else 'dnat.addr.prefix.addr', data['expr'][3]) - len_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3]) - if addr_tmp and len_tmp: - tran_addr += addr_tmp + '/' + str(len_tmp) + ' ' - - if tran_addr_json.get('port'): - if isinstance(tran_addr_json['port'],int): - tran_addr += 'port ' + str(tran_addr_json['port']) - - else: - if 'masquerade' in data['expr'][i]: - tran_addr = 'masquerade' - elif 'log' in data['expr'][i]: - continue - - if srcdest != '': - srcdests.append(srcdest) - srcdest = '' - else: - srcdests.append('any') - print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface)) - - for i in range(1, len(srcdests)): - print(format_nat_rule.format(' ', srcdests[i], ' ', ' ')) - - exit(0) -else: - parser.print_help() - exit(1) - diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py deleted file mode 100755 index 3c7a90205..000000000 --- a/src/op_mode/show_vrf.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import argparse -import jinja2 -from json import loads - -from vyos.util import cmd - -vrf_out_tmpl = """VRF name state mac address flags interfaces --------- ----- ----------- ----- ---------- -{%- for v in vrf %} -{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}} -{%- endfor %} - -""" - -def list_vrfs(): - command = 'ip -j -br link show type vrf' - answer = loads(cmd(command)) - return [_ for _ in answer if _] - -def list_vrf_members(vrf): - command = f'ip -j -br link show master {vrf}' - answer = loads(cmd(command)) - return [_ for _ in answer if _] - -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-e", "--extensive", action="store_true", - help="provide detailed vrf informatio") -parser.add_argument('interface', metavar='I', type=str, nargs='?', - help='interface to display') - -args = parser.parse_args() - -if args.extensive: - data = { 'vrf': [] } - for vrf in list_vrfs(): - name = vrf['ifname'] - if args.interface and name != args.interface: - continue - - vrf['members'] = [] - for member in list_vrf_members(name): - vrf['members'].append(member['ifname']) - data['vrf'].append(vrf) - - tmpl = jinja2.Template(vrf_out_tmpl) - print(tmpl.render(data)) - -else: - print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 8955e5a59..68dc5bc45 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.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 @@ -87,6 +87,7 @@ def reset_profile(profile, tunnel): print('Profile reset result: ' + ('success' if result == 0 else 'failed')) def debug_peer(peer, tunnel): + peer = peer.replace(':', '-') if not peer or peer == "all": debug_commands = [ "sudo ipsec statusall", @@ -109,7 +110,7 @@ def debug_peer(peer, tunnel): if not tunnel or tunnel == 'all': tunnel = '' - conn = get_peer_connections(peer, tunnel) + conns = get_peer_connections(peer, tunnel, return_all = (tunnel == '' or tunnel == 'all')) if not conns: print('Peer not found, aborting') diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py new file mode 100755 index 000000000..e3d944d90 --- /dev/null +++ b/src/op_mode/vrf.py @@ -0,0 +1,95 @@ +#!/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 +import jmespath +import sys +import typing + +from tabulate import tabulate +from vyos.util import cmd + +import vyos.opmode + + +def _get_raw_data(name=None): + """ + If vrf name is not set - get all VRFs + If vrf name is set - get only this name data + If vrf name set and not found - return [] + """ + output = cmd('sudo ip --json --brief link show type vrf') + data = json.loads(output) + if not data: + return [] + if name: + is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False + if is_vrf_exists: + output = cmd(f'sudo ip --json --brief link show dev {name}') + data = json.loads(output) + return data + return [] + return data + + +def _get_vrf_members(vrf: str) -> list: + """ + Get list of interface VRF members + :param vrf: str + :return: list + """ + output = cmd(f'sudo ip --json --brief link show master {vrf}') + answer = json.loads(output) + interfaces = [] + for data in answer: + if 'ifname' in data: + interfaces.append(data.get('ifname')) + return interfaces if len(interfaces) > 0 else ['n/a'] + + +def _get_formatted_output(raw_data): + data_entries = [] + for vrf in raw_data: + name = vrf.get('ifname') + state = vrf.get('operstate').lower() + hw_address = vrf.get('address') + flags = ','.join(vrf.get('flags')).lower() + members = ','.join(_get_vrf_members(name)) + data_entries.append([name, state, hw_address, flags, members]) + + headers = ["Name", "State", "MAC address", "Flags", "Interfaces"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def show(raw: bool, name: typing.Optional[str]): + vrf_data = _get_raw_data(name=name) + if not jmespath.search('[*].ifname', vrf_data): + return "VRF is not configured" + if raw: + return vrf_data + else: + return _get_formatted_output(vrf_data) + + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except ValueError as e: + print(e) + sys.exit(1) diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 84d719fda..049d59de7 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -17,11 +17,14 @@ import vyos.defaults from . graphql.queries import query from . graphql.mutations import mutation from . graphql.directives import directives_dict +from . utils.schema_from_op_mode import generate_op_mode_definitions from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers def generate_schema(): api_schema_dir = vyos.defaults.directories['api_schema'] + generate_op_mode_definitions() + type_defs = load_schema_from_path(api_schema_dir) schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict) diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index 0a9298f55..d8ceefae6 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -48,6 +48,14 @@ class ShowConfigDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_show_config_resolver) +class SystemStatusDirective(VyosDirective): + """ + Class providing implementation of 'system_status' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_system_status_resolver) + class ConfigFileDirective(VyosDirective): """ Class providing implementation of 'configfile' directive in schema. @@ -72,8 +80,27 @@ class ImageDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_image_resolver) +class GenOpQueryDirective(VyosDirective): + """ + Class providing implementation of 'genopquery' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_query_resolver) + +class GenOpMutationDirective(VyosDirective): + """ + Class providing implementation of 'genopmutation' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_gen_op_mutation_resolver) + directives_dict = {"configure": ConfigureDirective, "showconfig": ShowConfigDirective, + "systemstatus": SystemStatusDirective, "configfile": ConfigFileDirective, "show": ShowDirective, - "image": ImageDirective} + "image": ImageDirective, + "genopquery": GenOpQueryDirective, + "genopmutation": GenOpMutationDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 0c3eb702a..3e89fb239 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from .. import key_auth from api.graphql.recipes.session import Session mutation = ObjectType("Mutation") @@ -53,6 +54,19 @@ def make_mutation_resolver(mutation_name, class_name, session_func): } data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -61,7 +75,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -97,3 +111,7 @@ def make_config_file_resolver(mutation_name): def make_image_resolver(mutation_name): return make_prefix_resolver(mutation_name, prefix=['add', 'delete']) + +def make_gen_op_mutation_resolver(mutation_name): + class_name = mutation_name + return make_mutation_resolver(mutation_name, class_name, 'gen_op_mutation') diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index e1868091e..f6544709e 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,7 @@ from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state +from .. import key_auth from api.graphql.recipes.session import Session query = ObjectType("Query") @@ -53,6 +54,19 @@ def make_query_resolver(query_name, class_name, session_func): } data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + session = state.settings['app'].state.vyos_session # one may override the session functions with a local subclass @@ -61,7 +75,7 @@ def make_query_resolver(query_name, class_name, session_func): klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass - # name based templates + # name based functions klass = type(class_name, (Session,), {}) k = klass(session, data) method = getattr(k, session_func) @@ -84,6 +98,14 @@ def make_show_config_resolver(query_name): class_name = query_name return make_query_resolver(query_name, class_name, 'show_config') +def make_system_status_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'system_status') + def make_show_resolver(query_name): class_name = query_name return make_query_resolver(query_name, class_name, 'show') + +def make_gen_op_query_resolver(query_name): + class_name = query_name + return make_query_resolver(query_name, class_name, 'gen_op_query') diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql index 31ab26b9e..a7263114b 100644 --- a/src/services/api/graphql/graphql/schema/config_file.graphql +++ b/src/services/api/graphql/graphql/schema/config_file.graphql @@ -1,4 +1,5 @@ input SaveConfigFileInput { + key: String! fileName: String } @@ -13,6 +14,7 @@ type SaveConfigFileResult { } input LoadConfigFileInput { + key: String! fileName: String! } diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql index 25f091bfa..345c349ac 100644 --- a/src/services/api/graphql/graphql/schema/dhcp_server.graphql +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -1,4 +1,5 @@ input DhcpServerConfigInput { + key: String! sharedNetworkName: String subnet: String defaultRouter: String diff --git a/src/services/api/graphql/graphql/schema/firewall_group.graphql b/src/services/api/graphql/graphql/schema/firewall_group.graphql index d89904b9e..9454d2997 100644 --- a/src/services/api/graphql/graphql/schema/firewall_group.graphql +++ b/src/services/api/graphql/graphql/schema/firewall_group.graphql @@ -1,4 +1,5 @@ input CreateFirewallAddressGroupInput { + key: String! name: String! address: [String] } @@ -15,6 +16,7 @@ type CreateFirewallAddressGroupResult { } input UpdateFirewallAddressGroupMembersInput { + key: String! name: String! address: [String!]! } @@ -31,6 +33,7 @@ type UpdateFirewallAddressGroupMembersResult { } input RemoveFirewallAddressGroupMembersInput { + key: String! name: String! address: [String!]! } @@ -47,6 +50,7 @@ type RemoveFirewallAddressGroupMembersResult { } input CreateFirewallAddressIpv6GroupInput { + key: String! name: String! address: [String] } @@ -63,6 +67,7 @@ type CreateFirewallAddressIpv6GroupResult { } input UpdateFirewallAddressIpv6GroupMembersInput { + key: String! name: String! address: [String!]! } @@ -79,6 +84,7 @@ type UpdateFirewallAddressIpv6GroupMembersResult { } input RemoveFirewallAddressIpv6GroupMembersInput { + key: String! name: String! address: [String!]! } diff --git a/src/services/api/graphql/graphql/schema/image.graphql b/src/services/api/graphql/graphql/schema/image.graphql index 7d1b4f9d0..485033875 100644 --- a/src/services/api/graphql/graphql/schema/image.graphql +++ b/src/services/api/graphql/graphql/schema/image.graphql @@ -1,4 +1,5 @@ input AddSystemImageInput { + key: String! location: String! } @@ -14,6 +15,7 @@ type AddSystemImageResult { } input DeleteSystemImageInput { + key: String! name: String! } diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql index 32438b315..8a17d919f 100644 --- a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -1,4 +1,5 @@ input InterfaceEthernetConfigInput { + key: String! interface: String address: String replace: Boolean = true diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 952e46f34..624be2620 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -7,11 +7,17 @@ directive @configure on FIELD_DEFINITION directive @configfile on FIELD_DEFINITION directive @show on FIELD_DEFINITION directive @showconfig on FIELD_DEFINITION +directive @systemstatus on FIELD_DEFINITION directive @image on FIELD_DEFINITION +directive @genopquery on FIELD_DEFINITION +directive @genopmutation on FIELD_DEFINITION + +scalar Generic type Query { Show(data: ShowInput) : ShowResult @show ShowConfig(data: ShowConfigInput) : ShowConfigResult @showconfig + SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus } type Mutation { diff --git a/src/services/api/graphql/graphql/schema/show.graphql b/src/services/api/graphql/graphql/schema/show.graphql index c7709e48b..278ed536b 100644 --- a/src/services/api/graphql/graphql/schema/show.graphql +++ b/src/services/api/graphql/graphql/schema/show.graphql @@ -1,4 +1,5 @@ input ShowInput { + key: String! path: [String!]! } diff --git a/src/services/api/graphql/graphql/schema/show_config.graphql b/src/services/api/graphql/graphql/schema/show_config.graphql index 34afd2aa9..5a1fe43da 100644 --- a/src/services/api/graphql/graphql/schema/show_config.graphql +++ b/src/services/api/graphql/graphql/schema/show_config.graphql @@ -2,9 +2,9 @@ Use 'scalar Generic' for show config output, to avoid attempts to JSON-serialize in case of JSON output. """ -scalar Generic input ShowConfigInput { + key: String! path: [String!]! configFormat: String } diff --git a/src/services/api/graphql/graphql/schema/system_status.graphql b/src/services/api/graphql/graphql/schema/system_status.graphql new file mode 100644 index 000000000..be8d87535 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/system_status.graphql @@ -0,0 +1,18 @@ +""" +Use 'scalar Generic' for system status output, to avoid attempts to +JSON-serialize in case of JSON output. +""" + +input SystemStatusInput { + key: String! +} + +type SystemStatus { + result: Generic +} + +type SystemStatusResult { + data: SystemStatus + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/key_auth.py new file mode 100644 index 000000000..f756ed6d8 --- /dev/null +++ b/src/services/api/graphql/key_auth.py @@ -0,0 +1,18 @@ + +from . import state + +def check_auth(key_list, key): + if not key_list: + return None + key_id = None + for k in key_list: + if k['key'] == key: + key_id = k['id'] + return key_id + +def auth_required(key): + api_keys = None + api_keys = state.settings['app'].state.vyos_keys + key_id = check_auth(api_keys, key) + state.settings['app'].state.vyos_id = key_id + return key_id diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/recipes/queries/system_status.py new file mode 100755 index 000000000..8dadcc9f3 --- /dev/null +++ b/src/services/api/graphql/recipes/queries/system_status.py @@ -0,0 +1,38 @@ +#!/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 sys +import json +import importlib.util + +from vyos.defaults import directories + +from api.graphql.utils.util import load_op_mode_as_module + +def get_system_version() -> dict: + show_version = load_op_mode_as_module('version.py') + return show_version.show(raw=True, funny=False) + +def get_system_uptime() -> dict: + show_uptime = load_op_mode_as_module('show_uptime.py') + return show_uptime.get_raw_data() + +def get_system_ram_usage() -> dict: + show_ram = load_op_mode_as_module('memory.py') + return show_ram.show(raw=True) diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/recipes/session.py index 1f844ff70..ac185beb7 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/recipes/session.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,15 +13,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. +import os import json from ariadne import convert_camel_case_to_snake -import vyos.defaults from vyos.config import Config from vyos.configtree import ConfigTree +from vyos.defaults import directories from vyos.template import render +from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name + +op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -33,6 +38,12 @@ class Session: self._data = data self._name = convert_camel_case_to_snake(type(self).__name__) + try: + with open(op_mode_include_file) as f: + self._op_mode_list = json.loads(f.read()) + except Exception: + self._op_mode_list = None + def configure(self): session = self._session data = self._data @@ -40,7 +51,7 @@ class Session: tmpl_file = f'{func_base_name}.tmpl' cmd_file = f'/tmp/{func_base_name}.cmds' - tmpl_dir = vyos.defaults.directories['api_templates'] + tmpl_dir = directories['api_templates'] try: render(cmd_file, tmpl_file, data, location=tmpl_dir) @@ -136,3 +147,61 @@ class Session: raise error return res + + def system_status(self): + import api.graphql.recipes.queries.system_status as system_status + + session = self._session + data = self._data + + status = {} + status['host_name'] = session.show(['host', 'name']).strip() + status['version'] = system_status.get_system_version() + status['uptime'] = system_status.get_system_uptime() + status['ram'] = system_status.get_system_ram_usage() + + return status + + def gen_op_query(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(True, **data) + else: + res = func(True) + + return res + + def gen_op_mutation(self): + session = self._session + data = self._data + name = self._name + op_mode_list = self._op_mode_list + + # handle the case that the op-mode file name contains underscores: + if op_mode_list is None: + raise FileNotFoundError(f"No op-mode file list at '{op_mode_include_file}'") + (func_name, scriptname) = split_compound_op_mode_name(name, op_mode_list) + if scriptname == '': + raise FileNotFoundError(f"No op-mode file named in string '{name}'") + + mod = load_op_mode_as_module(f'{scriptname}') + func = getattr(mod, func_name) + if len(list(data)) > 0: + res = func(**data) + else: + res = func() + + return res diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py new file mode 100755 index 000000000..d27586747 --- /dev/null +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -0,0 +1,161 @@ +#!/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/>. +# +# +# A utility to generate GraphQL schema defintions from standardized op-mode +# scripts. + +import os +import json +import typing +from inspect import signature, getmembers, isfunction +from jinja2 import Template + +from vyos.defaults import directories +from . util import load_as_module, is_op_mode_function_name, is_show_function_name + +OP_MODE_PATH = directories['op_mode'] +SCHEMA_PATH = directories['api_schema'] +DATA_DIR = directories['data'] + +op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json') + +schema_data: dict = {'schema_name': '', + 'schema_fields': []} + +query_template = """ +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Query { + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery +} +""" + +mutation_template = """ +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Mutation { + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation +} +""" + +def _snake_to_pascal_case(name: str) -> str: + res = ''.join(map(str.title, name.split('_'))) + return res + +def _map_type_name(type_name: type, optional: bool = False) -> str: + if type_name == str: + return 'String!' if not optional else 'String = null' + if type_name == int: + return 'Int!' if not optional else 'Int = null' + if type_name == bool: + return 'Boolean!' if not optional else 'Boolean = false' + if typing.get_origin(type_name) == list: + if not optional: + return f'[{_map_type_name(typing.get_args(type_name)[0])}]!' + return f'[{_map_type_name(typing.get_args(type_name)[0])}]' + # typing.Optional is typing.Union[_, NoneType] + if (typing.get_origin(type_name) is typing.Union and + typing.get_args(type_name)[1] == type(None)): + return f'{_map_type_name(typing.get_args(type_name)[0], optional=True)}' + + # scalar 'Generic' is defined in schema.graphql + return 'Generic' + +def create_schema(func_name: str, base_name: str, func: callable) -> str: + sig = signature(func) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = _map_type_name(sig.parameters[k].annotation) + + # It is assumed that if one is generating a schema for a 'show_*' + # function, that 'get_raw_data' is present and 'raw' is desired. + if 'raw' in list(field_dict): + del field_dict['raw'] + + schema_fields = [] + for k,v in field_dict.items(): + schema_fields.append(k+': '+v) + + schema_data['schema_name'] = _snake_to_pascal_case(func_name + '_' + base_name) + schema_data['schema_fields'] = schema_fields + + if is_show_function_name(func_name): + j2_template = Template(query_template) + else: + j2_template = Template(mutation_template) + + res = j2_template.render(schema_data) + + return res + +def generate_op_mode_definitions(): + with open(op_mode_include_file) as f: + op_mode_files = json.load(f) + + for file in op_mode_files: + basename = os.path.splitext(file)[0].replace('-', '_') + module = load_as_module(basename, os.path.join(OP_MODE_PATH, file)) + + funcs = getmembers(module, isfunction) + funcs = list(filter(lambda ft: is_op_mode_function_name(ft[0]), funcs)) + + funcs_dict = {} + for (name, thunk) in funcs: + funcs_dict[name] = thunk + + results = [] + for name,func in funcs_dict.items(): + res = create_schema(name, basename, func) + results.append(res) + + out = '\n'.join(results) + with open(f'{SCHEMA_PATH}/{basename}.graphql', 'w') as f: + f.write(out) + +if __name__ == '__main__': + generate_op_mode_definitions() diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/utils/util.py new file mode 100644 index 000000000..073126853 --- /dev/null +++ b/src/services/api/graphql/utils/util.py @@ -0,0 +1,76 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import importlib.util + +from vyos.defaults import directories + +def load_as_module(name: str, path: str): + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + +def load_op_mode_as_module(name: str): + path = os.path.join(directories['op_mode'], name) + name = os.path.splitext(name)[0].replace('-', '_') + return load_as_module(name, path) + +def is_op_mode_function_name(name): + if re.match(r"^(show|clear|reset|restart)", name): + return True + return False + +def is_show_function_name(name): + if re.match(r"^show", name): + return True + return False + +def _nth_split(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:n]), delim.join(groups[n:])) + +def _nth_rsplit(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:l-n]), delim.join(groups[l-n:])) + +# Since we have mangled possible hyphens in the file name while constructing +# the snake case of the query/mutation name, we will need to recover the +# file name by searching with mangling: +def _filter_on_mangled(test): + def func(elem): + mangle = os.path.splitext(elem)[0].replace('-', '_') + return test == mangle + return func + +# Find longest name in concatenated string that matches the basename of an +# op-mode script. Should one prefer to concatenate in the reverse order +# (script_name + '_' + function_name), use _nth_rsplit. +def split_compound_op_mode_name(name: str, files: list): + for i in range(1, name.count('_') + 1): + pair = _nth_split('_', i, name) + f = list(filter(_filter_on_mangled(pair[1]), files)) + if f: + pair = (pair[0], f[0]) + return pair + return (name, '') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index e9b904ba8..af8837e1e 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -654,11 +654,13 @@ def graphql_init(fast_api_app): schema = generate_schema() + in_spec = app.state.vyos_introspection + if app.state.vyos_origins: origins = app.state.vyos_origins - app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) + app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) else: - app.add_route('/graphql', GraphQL(schema, debug=True)) + app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec)) ### @@ -684,6 +686,7 @@ if __name__ == '__main__': app.state.vyos_debug = server_config['debug'] app.state.vyos_gql = server_config['gql'] + app.state.vyos_introspection = server_config['introspection'] app.state.vyos_strict = server_config['strict'] app.state.vyos_origins = server_config.get('cors', {}).get('origins', []) diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service index 7e0bee8e1..ffb4fe32c 100644 --- a/src/systemd/wpa_supplicant-macsec@.service +++ b/src/systemd/wpa_supplicant-macsec@.service @@ -1,17 +1,18 @@ [Unit] -Description=WPA supplicant daemon (macsec-specific version) +Description=WPA supplicant daemon (MACsec-specific version) Requires=sys-subsystem-net-devices-%i.device ConditionPathExists=/run/wpa_supplicant/%I.conf After=vyos-router.service RequiresMountsFor=/run -# NetworkManager users will probably want the dbus version instead. - [Service] Type=simple WorkingDirectory=/run/wpa_supplicant PIDFile=/run/wpa_supplicant/%I.pid -ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -P/run/wpa_supplicant/%I.pid -i%I +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=2 [Install] WantedBy=multi-user.target diff --git a/src/validators/ipv6-address-exclude b/src/validators/ipv6-address-exclude new file mode 100755 index 000000000..be1d3db25 --- /dev/null +++ b/src/validators/ipv6-address-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6-address "${arg:1}" diff --git a/src/validators/ipv6-prefix-exclude b/src/validators/ipv6-prefix-exclude new file mode 100755 index 000000000..6fa4f1d8d --- /dev/null +++ b/src/validators/ipv6-prefix-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6-prefix "${arg:1}" |