diff options
65 files changed, 876 insertions, 359 deletions
| diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 5f49be781..db13eeb5a 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -8,6 +8,7 @@  "neighbor.py",  "openconnect.py",  "route.py", +"ipsec.py",  "version.py",  "vrf.py"  ] diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 2fe04b4ff..28714c7a7 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -7,6 +7,17 @@  {% 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 %} +{# Port #} +{% if config.source.port is vyos_defined and config.source.port.startswith('!') %} +{%     set src_port  = 'sport != { ' ~ config.source.port.replace('!','') ~ ' }' %} +{% else %} +{%     set src_port  = 'sport { ' ~ config.source.port ~ ' }' if config.source.port is vyos_defined %} +{% endif %} +{% if config.destination.port is vyos_defined and config.destination.port.startswith('!') %} +{%     set dst_port  = 'dport != { ' ~ config.destination.port.replace('!','') ~ ' }' %} +{% else %} +{%     set dst_port  = 'dport { ' ~ config.destination.port ~ ' }' if config.destination.port is vyos_defined %} +{% endif %}  {% if chain is vyos_defined('PREROUTING') %}  {%     set comment   = 'DST-NAT66-' ~ rule %}  {%     set base_log  = '[NAT66-DST-' ~ rule %} @@ -36,6 +47,14 @@  {%     endif   %}  {%     set interface = ' oifname "' ~ config.outbound_interface ~ '"' if config.outbound_interface is vyos_defined else '' %}  {% endif %} +{% set trns_port = ':' ~ config.translation.port if config.translation.port is vyos_defined %} +{# protocol has a default value thus it is always present #} +{% if config.protocol is vyos_defined('tcp_udp') %} +{%     set protocol  = 'tcp' %} +{%     set comment   = comment ~ ' tcp_udp' %} +{% else %} +{%     set protocol  = config.protocol %} +{% endif %}  {% if config.log is vyos_defined %}  {%     if config.translation.address is vyos_defined('masquerade') %}  {%         set log = base_log ~ '-MASQ]' %} @@ -43,6 +62,11 @@  {%         set log = base_log ~ ']' %}  {%     endif %}  {% endif %} +{% if config.exclude is vyos_defined %} +{#     rule has been marked as 'exclude' thus we simply return here #} +{%     set trns_addr = 'return' %} +{%     set trns_port = '' %} +{% endif %}  {% set output = 'add rule ip6 nat ' ~ chain ~ interface %}  {# Count packets #}  {% set output = output ~ ' counter' %} @@ -54,12 +78,18 @@  {% if src_prefix is vyos_defined %}  {%     set output = output ~ ' ' ~ src_prefix %}  {% endif %} +{% if dst_port is vyos_defined %} +{%     set output = output ~ ' ' ~ protocol ~ ' ' ~ dst_port %} +{% 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 src_port is vyos_defined %} +{%     set output = output ~ ' ' ~ protocol ~ ' ' ~ src_port %} +{% endif %}  {% if dest_address is vyos_defined %}  {%     set output = output ~ ' ' ~ dest_address %}  {% endif %} @@ -70,11 +100,22 @@  {% if trns_address is vyos_defined %}  {%     set output = output ~ ' ' ~ trns_address %}  {% endif %} +{% if trns_port is vyos_defined %} +{#     Do not add a whitespace here, translation port must be directly added after IP address #} +{#     e.g. 2001:db8::1:3389                                                                   #} +{%     set output = output ~ trns_port %} +{% endif %}  {% if comment is vyos_defined %}  {%     set output = output ~ ' comment "' ~ comment ~ '"' %}  {% endif %}  {{ log_output if log_output is vyos_defined }}  {{ output }} +{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #} +{% if config.protocol is vyos_defined('tcp_udp') %} +{#     Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #} +{{ log_output | replace('tcp ', 'udp ') if log_output is vyos_defined }} +{{ output | replace('tcp ', 'udp ') }} +{% endif %}  {% endmacro %}  # Start with clean NAT table diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 2ab7c8596..808e9dbe7 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -38,6 +38,9 @@  {% if config.disable_capability_negotiation is vyos_defined %}   neighbor {{ neighbor }} dont-capability-negotiate  {% endif %} +{% if config.disable_connected_check is vyos_defined %} + neighbor {{ neighbor }} disable-connected-check +{% endif %}  {% if config.ebgp_multihop is vyos_defined %}   neighbor {{ neighbor }} ebgp-multihop {{ config.ebgp_multihop }}  {% endif %} diff --git a/data/templates/monitoring/override.conf.j2 b/data/templates/monitoring/override.conf.j2 deleted file mode 100644 index 9f1b4ebec..000000000 --- a/data/templates/monitoring/override.conf.j2 +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -After=vyos-router.service -ConditionPathExists=/run/telegraf/vyos-telegraf.conf -[Service] -Environment=INFLUX_TOKEN={{ influxdb.authentication.token }} -CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN -AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN diff --git a/data/templates/telegraf/override.conf.j2 b/data/templates/telegraf/override.conf.j2 new file mode 100644 index 000000000..d30bb19de --- /dev/null +++ b/data/templates/telegraf/override.conf.j2 @@ -0,0 +1,15 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/telegraf/telegraf.conf + +[Service] +ExecStart= +ExecStart={{ vrf_command }}/usr/bin/telegraf --config /run/telegraf/telegraf.conf --config-directory /etc/telegraf/telegraf.d --pidfile /run/telegraf/telegraf.pid +PIDFile=/run/telegraf/telegraf.pid +EnvironmentFile= +Environment=INFLUX_TOKEN={{ influxdb.authentication.token }} +CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN CAP_BPF CAP_DAC_OVERRIDE +AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN + diff --git a/data/templates/monitoring/syslog_telegraf.j2 b/data/templates/telegraf/syslog_telegraf.j2 index cdcbd92a4..cdcbd92a4 100644 --- a/data/templates/monitoring/syslog_telegraf.j2 +++ b/data/templates/telegraf/syslog_telegraf.j2 diff --git a/data/templates/monitoring/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 6b395692b..6b395692b 100644 --- a/data/templates/monitoring/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 diff --git a/interface-definitions/include/firewall/tcp-flags.xml.i b/interface-definitions/include/firewall/tcp-flags.xml.i index b99896687..5a7b5a8d3 100644 --- a/interface-definitions/include/firewall/tcp-flags.xml.i +++ b/interface-definitions/include/firewall/tcp-flags.xml.i @@ -114,6 +114,23 @@          </node>        </children>      </node> +    <leafNode name="mss"> +      <properties> +        <help>Maximum segment size (MSS)</help> +        <valueHelp> +          <format>u32:1-16384</format> +          <description>Maximum segment size</description> +        </valueHelp> +        <valueHelp> +          <format><min>-<max></format> +          <description>TCP MSS range (use '-' as delimiter)</description> +        </valueHelp> +        <constraint> +          <validator name="numeric" argument="--range 1-16384"/> +          <validator name="range" argument="--min=1 --max=16384"/> +        </constraint> +      </properties> +    </leafNode>    </children>  </node>  <!-- include end --> diff --git a/interface-definitions/include/nat/protocol.xml.i b/interface-definitions/include/nat/protocol.xml.i new file mode 100644 index 000000000..54e7ff00d --- /dev/null +++ b/interface-definitions/include/nat/protocol.xml.i @@ -0,0 +1,34 @@ +<!-- include start from nat/protocol.xml.i --> +<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> +<!-- include end --> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index bde1a6f8d..dab4543e0 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -50,6 +50,7 @@                    </completionHelp>                  </properties>                </leafNode> +              #include <include/nat/protocol.xml.i>                <node name="destination">                  <properties>                    <help>IPv6 destination prefix options</help> @@ -72,6 +73,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="source"> @@ -96,6 +98,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="translation"> @@ -128,6 +131,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-translation-port.xml.i>                  </children>                </node>              </children> @@ -179,6 +183,7 @@                    </completionHelp>                  </properties>                </leafNode> +              #include <include/nat/protocol.xml.i>                <node name="destination">                  <properties>                    <help>IPv6 destination prefix options</help> @@ -211,6 +216,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="source"> @@ -245,6 +251,7 @@                        </constraint>                      </properties>                    </leafNode> +                #include <include/nat-port.xml.i>                  </children>                </node>                <node name="translation"> @@ -269,6 +276,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-translation-port.xml.i>                  </children>                </node>              </children> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cc1de609d..e794c4b90 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -392,7 +392,7 @@                      <description>Prefix to match against</description>                    </valueHelp>                    <constraint> -                    <validator name="ip-prefix"/> +                    <validator name="ipv4-prefix"/>                    </constraint>                  </properties>                </leafNode> diff --git a/interface-definitions/service-monitoring-telegraf.xml.in b/interface-definitions/service-monitoring-telegraf.xml.in index 36f40a539..68215dba4 100644 --- a/interface-definitions/service-monitoring-telegraf.xml.in +++ b/interface-definitions/service-monitoring-telegraf.xml.in @@ -10,7 +10,7 @@          <children>            <node name="telegraf" owner="${vyos_conf_scripts_dir}/service_monitoring_telegraf.py">              <properties> -              <help>Telegraf monitoring</help> +              <help>Telegraf metric collector</help>              </properties>              <children>                <node name="influxdb"> @@ -228,27 +228,7 @@                        </constraint>                      </properties>                    </leafNode> -                  <leafNode name="listen-address"> -                    <properties> -                      <help>Local IP addresses to listen on</help> -                      <completionHelp> -                        <script>${vyos_completion_dir}/list_local_ips.sh --both</script> -                      </completionHelp> -                      <valueHelp> -                        <format>ipv4</format> -                        <description>IPv4 address to listen for incoming connections</description> -                      </valueHelp> -                      <valueHelp> -                        <format>ipv6</format> -                        <description>IPv6 address to listen for incoming connections</description> -                      </valueHelp> -                      <constraint> -                        <validator name="ipv4-address"/> -                        <validator name="ipv6-address"/> -                        <validator name="ipv6-link-local"/> -                      </constraint> -                    </properties> -                  </leafNode> +                  #include <include/listen-address.xml.i>                    <leafNode name="metric-version">                      <properties>                        <help>Metric version control mapping from Telegraf to Prometheus format</help> @@ -291,21 +271,10 @@                        </leafNode>                      </children>                    </node> -                  <leafNode name="url"> -                    <properties> -                      <help>Remote URL</help> -                      <valueHelp> -                        <format>url</format> -                        <description>Remote URL to Splunk collector</description> -                      </valueHelp> -                      <constraint> -                        <regex>^(http(s?):\/\/.*):(\d*)\/?(.*)</regex> -                      </constraint> -                      <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> -                    </properties> -                  </leafNode> +                  #include <include/monitoring/url.xml.i>                  </children>                </node> +              #include <include/interface/vrf.xml.i>              </children>            </node>          </children> diff --git a/interface-definitions/system-proxy.xml.in b/interface-definitions/system-proxy.xml.in index 1c06b347f..8fb6bfae5 100644 --- a/interface-definitions/system-proxy.xml.in +++ b/interface-definitions/system-proxy.xml.in @@ -11,7 +11,7 @@              <properties>                <help>Proxy URL</help>                <constraint> -                <regex>http:\/\/[a-z0-9\.]+</regex> +                <regex>http(s)?:\/\/[a-z0-9-\.]+</regex>                </constraint>              </properties>            </leafNode> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 8a02e1f08..975d20465 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -224,6 +224,43 @@              </properties>              <command>journalctl --no-hostname --boot --follow --unit ssh.service</command>            </leafNode> +          <node name="vpn"> +            <properties> +              <help>Show log for Virtual Private Network (VPN)</help> +            </properties> +            <children> +              <leafNode name="all"> +                <properties> +                  <help>Monitor last lines of ALL VPNs</help> +                </properties> +                <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service --unit accel-ppp@*.service</command> +              </leafNode> +              <leafNode name="ipsec"> +                <properties> +                  <help>Monitor last lines of IPSec</help> +                </properties> +                <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service</command> +              </leafNode> +              <leafNode name="l2tp"> +                <properties> +                  <help>Monitor last lines of L2TP</help> +                </properties> +                <command>journalctl --no-hostname --boot --follow --unit accel-ppp@l2tp.service</command> +              </leafNode> +              <leafNode name="pptp"> +                <properties> +                  <help>Monitor last lines of PPTP</help> +                </properties> +                <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pptp.service</command> +              </leafNode> +              <leafNode name="sstp"> +                <properties> +                  <help>Monitor last lines of SSTP</help> +                </properties> +                <command>journalctl --no-hostname --boot --follow --unit accel-ppp@sstp.service</command> +              </leafNode> +            </children> +          </node>          </children>        </node>      </children> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index 7148c1128..ce0544390 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}/nat.py show_rules --direction source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet</command>                </node>                <node name="statistics">                  <properties>                    <help>Show statistics for configured source NAT rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source --family inet</command>                </node>                <node name="translations">                  <properties> @@ -45,7 +45,7 @@                      <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet</command>                </node>              </children>            </node> @@ -58,7 +58,7 @@                  <properties>                    <help>Show configured destination NAT rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet</command>                </node>                <node name="statistics">                  <properties> @@ -87,7 +87,7 @@                      <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet</command>                </node>              </children>            </node> diff --git a/op-mode-definitions/nat66.xml.in b/op-mode-definitions/nat66.xml.in index 1ec46eb11..25aa04d59 100644 --- a/op-mode-definitions/nat66.xml.in +++ b/op-mode-definitions/nat66.xml.in @@ -16,7 +16,7 @@                  <properties>                    <help>Show configured source NAT66 rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/show_nat66_rules.py --source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet6</command>                </node>                <node name="statistics">                  <properties> @@ -45,7 +45,7 @@                      <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6</command>                </node>              </children>            </node> @@ -58,7 +58,7 @@                  <properties>                    <help>Show configured destination NAT66 rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/show_nat66_rules.py --destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet6</command>                </node>                <node name="statistics">                  <properties> @@ -87,7 +87,7 @@                      <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6</command>                </node>              </children>            </node> diff --git a/op-mode-definitions/show-conntrack.xml.in b/op-mode-definitions/show-conntrack.xml.in index 8d921e6a5..4cdcffcdb 100644 --- a/op-mode-definitions/show-conntrack.xml.in +++ b/op-mode-definitions/show-conntrack.xml.in @@ -7,6 +7,12 @@            <help>Show conntrack tables entries</help>          </properties>          <children> +          <node name="statistics"> +            <properties> +              <help>Show conntrack statistics</help> +            </properties> +            <command>sudo ${vyos_op_scripts_dir}/conntrack.py show_statistics</command> +          </node>            <node name="table">              <properties>                <help>Show conntrack entries for table</help> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 24a1b5f3e..ebd198215 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -380,19 +380,19 @@                  <properties>                    <help>Show log for ALL</help>                  </properties> -                <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon -e accel -e pptpd -e ppp</command> +                <command>journalctl --no-hostname --boot --unit strongswan-starter.service --unit accel-ppp@*.service</command>                </leafNode>                <leafNode name="ipsec">                  <properties>                    <help>Show log for IPSec</help>                  </properties> -                <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon</command> +                <command>journalctl --no-hostname --boot --unit strongswan-starter.service</command>                </leafNode>                <leafNode name="l2tp">                  <properties>                    <help>Show log for L2TP</help>                  </properties> -                <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e remote-access-aaa-win -e remote-access-zzz-mac -e accel-l2tp -e ppp</command> +                <command>journalctl --no-hostname --boot --unit accel-ppp@l2tp.service</command>                </leafNode>                <leafNode name="pptp">                  <properties> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index a98cf8ff2..8c9e76651 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -187,7 +187,7 @@                      <command>if pgrep charon >/dev/null ; then sudo /usr/sbin/ipsec statusall ; else echo "IPSec process not running" ; fi</command>                    </node>                  </children> -                <command>if pgrep charon >/dev/null ; then sudo ${vyos_op_scripts_dir}/show_ipsec_sa.py ; else echo "IPSec process not running" ; fi</command> +                <command>if pgrep charon >/dev/null ; then sudo ${vyos_op_scripts_dir}/ipsec.py show_sa ; else echo "IPSec process not running" ; fi</command>                </node>                <node name="state">                  <properties> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 8f822a97d..912bc94f2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -295,11 +295,18 @@ def is_source_interface(conf, interface, intftype=None):      """      ret_val = None      intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan'] -    if intftype not in intftypes + [None]: +    if not intftype: +        intftype = intftypes + +    if isinstance(intftype, str): +        intftype = [intftype] +    elif not isinstance(intftype, list): +        raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!') + +    if not all(x in intftypes for x in intftype):          raise ValueError(f'unknown interface type "{intftype}" or it can not '              'have a source-interface') -    intftype = intftypes if intftype == None else [intftype]      for it in intftype:          base = ['interfaces', it]          for intf in conf.list_nodes(base): diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 2ab3cb408..447ec795c 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -295,6 +295,12 @@ def verify_source_interface(config):          raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface '                            f'is already a member of bond "{bond_name}"!') +    if 'is_source_interface' in config: +        tmp = config['is_source_interface'] +        src_ifname = config['source_interface'] +        raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ +                          f'belongs to interface "{tmp}"!') +  def verify_dhcpv6(config):      """      Common helper function used by interface implementations to perform diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 09ae73eac..6894fc4da 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -26,7 +26,7 @@ directories = {    "templates": "/usr/share/vyos/templates/",    "certbot": "/config/auth/letsencrypt",    "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", -  "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/", +  "api_templates": "/usr/libexec/vyos/services/api/graphql/session/templates/",    "vyos_udev_dir": "/run/udev/vyos"  } diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 3e2de4c3f..663c4394a 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -297,6 +297,11 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):      if tcp_flags:          output.append(parse_tcp_flags(tcp_flags)) +    # TCP MSS +    tcp_mss = dict_search_args(rule_conf, 'tcp', 'mss') +    if tcp_mss: +        output.append(f'tcp option maxseg size {tcp_mss}') +      output.append('counter')      if 'set' in rule_conf: diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 758967fbc..aa818bc5f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -295,8 +295,24 @@ class BridgeIf(Interface):                  self.del_port(member)          # enable/disable Vlan Filter -        vlan_filter = '1' if 'enable_vlan' in config else '0' -        self.set_vlan_filter(vlan_filter) +        tmp = '1' if 'enable_vlan' in config else '0' +        self.set_vlan_filter(tmp) + +        # add VLAN interfaces to local 'parent' bridge to allow forwarding +        if 'enable_vlan' in config: +            for vlan in config.get('vif_remove', {}): +                # Remove old VLANs from the bridge +                cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self' +                self._cmd(cmd) + +            for vlan in config.get('vif', {}): +                cmd = f'bridge vlan add dev {self.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 {self.ifname} vid 1 pvid untagged self' +            self._cmd(cmd)          tmp = dict_search('member.interface', config)          if tmp: diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index 63ffc8069..437fe0cae 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-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 @@ -14,6 +14,7 @@  # License along with this library.  If not, see <http://www.gnu.org/licenses/>.  from vyos.ifconfig.interface import Interface +from vyos.validate import assert_range  from vyos.util import get_interface_config  @Interface.register @@ -27,6 +28,21 @@ class PPPoEIf(Interface):          },      } +    _sysfs_get = { +        **Interface._sysfs_get,**{ +            'accept_ra_defrtr': { +                'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', +            } +        } +    } + +    _sysfs_set = {**Interface._sysfs_set, **{ +        'accept_ra_defrtr': { +            'validate': lambda value: assert_range(value, 0, 2), +            'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', +        }, +    }} +      def _remove_routes(self, vrf=None):          # Always delete default routes when interface is removed          vrf_cmd = '' @@ -70,6 +86,21 @@ class PPPoEIf(Interface):          """ Get a synthetic MAC address. """          return self.get_mac_synthetic() +    def set_accept_ra_defrtr(self, enable): +        """ +        Learn default router in Router Advertisement. +        1: enabled +        0: disable + +        Example: +        >>> from vyos.ifconfig import PPPoEIf +        >>> PPPoEIf('pppoe1').set_accept_ra_defrtr(0) +        """ +        tmp = self.get_interface('accept_ra_defrtr') +        if tmp == enable: +            return None +        self.set_interface('accept_ra_defrtr', enable) +      def update(self, config):          """ General helper function which works on a dictionary retrived by          get_config_dict(). It's main intention is to consolidate the scattered @@ -107,6 +138,10 @@ class PPPoEIf(Interface):              tmp = config['vrf']              vrf = f'-c "vrf {tmp}"' +        # learn default router in Router Advertisement. +        tmp = '0' if 'no_default_route' in config else '1' +        self.set_accept_ra_defrtr(tmp) +          if 'no_default_route' not in config:              # Set default route(s) pointing to PPPoE interface              distance = config['default_route_distance'] diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 91f667b65..5e98cd510 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -88,7 +88,7 @@ class Section:          raise ValueError(f'No type found for interface name: {name}')      @classmethod -    def _intf_under_section (cls,section=''): +    def _intf_under_section (cls,section='',vlan=True):          """          return a generator with the name of the configured interface          which are under a section @@ -103,6 +103,9 @@ class Section:              if section and ifsection != section:                  continue +            if vlan == False and '.' in ifname: +                continue +              yield ifname      @classmethod @@ -135,13 +138,14 @@ class Section:          return l      @classmethod -    def interfaces(cls, section=''): +    def interfaces(cls, section='', vlan=True):          """          return a list of the name of the configured interface which are under a section -        if no section is provided, then it returns all configured interfaces +        if no section is provided, then it returns all configured interfaces. +        If vlan is True, also Vlan subinterfaces will be returned          """ -        return cls._sort_interfaces(cls._intf_under_section(section)) +        return cls._sort_interfaces(cls._intf_under_section(section, vlan))      @classmethod      def _intf_with_feature(cls, feature=''): diff --git a/python/vyos/util.py b/python/vyos/util.py index c1459f02a..325b630bc 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -823,6 +823,32 @@ def dict_search_recursive(dict_object, key, path=[]):              for x in dict_search_recursive(j, key, new_path):                  yield x +def convert_data(data): +    """Convert multiple types of data to types usable in CLI + +    Args: +        data (str | bytes | list | OrderedDict): input data + +    Returns: +        str | list | dict: converted data +    """ +    from collections import OrderedDict + +    if isinstance(data, str): +        return data +    if isinstance(data, bytes): +        return data.decode() +    if isinstance(data, list): +        list_tmp = [] +        for item in data: +            list_tmp.append(convert_data(item)) +        return list_tmp +    if isinstance(data, OrderedDict): +        dict_tmp = {} +        for key, value in data.items(): +            dict_tmp[key] = convert_data(value) +        return dict_tmp +  def get_bridge_fdb(interface):      """ Returns the forwarding database entries for a given interface """      if not os.path.exists(f'/sys/class/net/{interface}'): diff --git a/smoketest/configs/service-https b/smoketest/configs/service-https new file mode 100644 index 000000000..d478d5731 --- /dev/null +++ b/smoketest/configs/service-https @@ -0,0 +1,55 @@ +interfaces { +    ethernet eth0 { +        address 192.168.150.1/24 +    } +} +service { +    https { +        certificates { +            system-generated-certificate { +                lifetime 365 +            } +        } +    } +} +system { +    config-management { +        commit-revisions 100 +    } +    console { +        device ttyS0 { +            speed 115200 +        } +    } +    host-name vyos +    login { +        user vyos { +            authentication { +                encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ +                plaintext-password "" +            } +        } +    } +    ntp { +        server time1.vyos.net { +        } +        server time2.vyos.net { +        } +        server time3.vyos.net { +        } +    } +    syslog { +        global { +            facility all { +                level info +            } +            facility protocols { +                level debug +            } +        } +    } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.4-rolling-202106290839 diff --git a/smoketest/configs/pki-misc b/smoketest/configs/vpn-openconnect-sstp index c90226a2a..45e6dd9b2 100644 --- a/smoketest/configs/pki-misc +++ b/smoketest/configs/vpn-openconnect-sstp @@ -3,15 +3,6 @@ interfaces {          address 192.168.150.1/24      }  } -service { -    https { -        certificates { -            system-generated-certificate { -                lifetime 365 -            } -        } -    } -}  system {      config-management {          commit-revisions 100 diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 4de90e1ec..684a07681 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -177,6 +177,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.verify_nftables(nftables_search, 'ip filter')      def test_basic_rules(self): +        mss_range = '501-1460'          self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop'])          self.cli_set(['firewall', 'name', 'smoketest', 'enable-default-log'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) @@ -203,6 +204,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'destination', 'port', '22'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'count', '10'])          self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'time', 'minute']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'action', 'accept']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'protocol', 'tcp']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'flags', 'syn']) +        self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'mss', mss_range])          self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) @@ -214,7 +219,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):              ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'log prefix "[smoketest-2-R]" level err', 'ip ttl > 102', 'reject'],              ['tcp dport { 22 }', 'limit rate 5/minute', 'return'],              ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'], -            ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'] +            ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'], +            [f'tcp flags & syn == syn tcp option maxseg size {mss_range}']          ]          self.verify_nftables(nftables_search, 'ip filter') diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 8f711af20..6d7af78eb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -19,6 +19,7 @@ import json  import unittest  from base_interfaces_test import BasicInterfaceTest +from copy import deepcopy  from glob import glob  from netifaces import interfaces @@ -224,85 +225,78 @@ 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 +        vifs = ['10', '20', '30', '40'] +        native_vlan = '20' +          # Add member interface to bridge and set VLAN filter          for interface in self._interfaces:              base = self._base_path + [interface]              self.cli_set(base + ['enable-vlan'])              self.cli_set(base + ['address', '192.0.2.1/24']) -            self.cli_set(base + ['vif', str(vif_vlan), 'address', '192.0.3.1/24']) -            self.cli_set(base + ['vif', str(vif_vlan), 'mtu', self._mtu]) -            vlan_id = 101 -            allowed_vlan = 2 -            allowed_vlan_range = '4-9' -            # assign members to bridge interface +            for vif in vifs: +                self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24']) +                self.cli_set(base + ['vif', vif, 'mtu', self._mtu]) +              for member in self._members:                  base_member = base + ['member', 'interface', member] -                self.cli_set(base_member + ['allowed-vlan', str(allowed_vlan)]) -                self.cli_set(base_member + ['allowed-vlan', allowed_vlan_range]) -                self.cli_set(base_member + ['native-vlan', str(vlan_id)]) -                vlan_id += 1 +                self.cli_set(base_member + ['native-vlan', native_vlan]) +                for vif in vifs: +                    self.cli_set(base_member + ['allowed-vlan', vif])          # commit config          self.cli_commit() +        def _verify_members(interface, members) -> None: +            # check member interfaces are added on the bridge +            bridge_members = [] +            for tmp in glob(f'/sys/class/net/{interface}/lower_*'): +                bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + +            self.assertListEqual(sorted(members), sorted(bridge_members)) + +        def _check_vlan_filter(interface, vifs) -> None: +            configured_vlan_ids = [] + +            bridge_json = cmd(f'bridge -j vlan show dev {interface}') +            bridge_json = json.loads(bridge_json) +            self.assertIsNotNone(bridge_json) + +            for tmp in bridge_json: +                self.assertIn('vlans', tmp) + +                for vlan in tmp['vlans']: +                    self.assertIn('vlan', vlan) +                    configured_vlan_ids.append(str(vlan['vlan'])) + +                    # Verify native VLAN ID has 'PVID' flag set on individual member ports +                    if not interface.startswith('br') and str(vlan['vlan']) == native_vlan: +                        self.assertIn('flags', vlan) +                        self.assertIn('PVID', vlan['flags']) + +            self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs)) +          # 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 and verify proper -        # VLAN filter setup -        _check_vlan_filter() +        # Obtain status information and verify proper VLAN filter setup. +        # First check if all members are present, second check if all VLANs +        # are assigned on the parend bridge interface, third verify all the +        # VLANs are properly setup on the downstream "member" ports +        for interface in self._interfaces: +            # check member interfaces are added on the bridge +            _verify_members(interface, self._members) -        # check member interfaces are added on the bridge -        _verify_members() +            # Check if all VLAN ids are properly set up. Bridge interface always +            # has native VLAN 1 +            tmp = deepcopy(vifs) +            tmp.append('1') +            _check_vlan_filter(interface, tmp) + +            for member in self._members: +                _check_vlan_filter(member, vifs)          # change member interface description to trigger config update,          # VLANs must still exist (T4565) @@ -313,12 +307,22 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):          # commit config          self.cli_commit() -        # check member interfaces are added on the bridge -        _verify_members() +        # Obtain status information and verify proper VLAN filter setup. +        # First check if all members are present, second check if all VLANs +        # are assigned on the parend bridge interface, third verify all the +        # VLANs are properly setup on the downstream "member" ports +        for interface in self._interfaces: +            # check member interfaces are added on the bridge +            _verify_members(interface, self._members) + +            # Check if all VLAN ids are properly set up. Bridge interface always +            # has native VLAN 1 +            tmp = deepcopy(vifs) +            tmp.append('1') +            _check_vlan_filter(interface, tmp) -        # Execute the program to obtain status information and verify proper -        # VLAN filter setup -        _check_vlan_filter() +            for member in self._members: +                _check_vlan_filter(member, vifs)          # delete all members          for interface in self._interfaces: @@ -337,7 +341,6 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):              for member in self._members:                  self.assertNotIn(member, bridge_members) -      def test_bridge_vif_members(self):          # T2945: ensure that VIFs are not dropped from bridge          vifs = ['300', '400'] diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 4b5625569..c5db066db 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -131,6 +131,30 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):          self.verify_nftables(nftables_search, 'ip6 nat') +    def test_destination_nat66_protocol(self): +        translation_address = '2001:db8:1111::1' +        source_prefix = '2001:db8:2222::/64' +        dport = '4545' +        sport = '8080' +        tport = '5555' +        proto = 'tcp' +        self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) +        self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dport]) +        self.cli_set(dst_path + ['rule', '1', 'source', 'address', source_prefix]) +        self.cli_set(dst_path + ['rule', '1', 'source', 'port', sport]) +        self.cli_set(dst_path + ['rule', '1', 'protocol', proto]) +        self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address]) +        self.cli_set(dst_path + ['rule', '1', 'translation', 'port', tport]) + +        # check validate() - outbound-interface must be defined +        self.cli_commit() + +        nftables_search = [ +            ['iifname "eth1"', 'tcp dport { 4545 } ip6 saddr 2001:db8:2222::/64 tcp sport { 8080 } dnat to 2001:db8:1111::1:5555'] +        ] + +        self.verify_nftables(nftables_search, 'ip6 nat') +      def test_destination_nat66_prefix(self):          destination_prefix = 'fc00::/64'          translation_prefix = 'fc01::/64' @@ -176,6 +200,30 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):          self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])          self.cli_commit() +    def test_source_nat66_protocol(self): +        translation_address = '2001:db8:1111::1' +        source_prefix = '2001:db8:2222::/64' +        dport = '9999' +        sport = '8080' +        tport = '80' +        proto = 'tcp' +        self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'eth1']) +        self.cli_set(src_path + ['rule', '1', 'destination', 'port', dport]) +        self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) +        self.cli_set(src_path + ['rule', '1', 'source', 'port', sport]) +        self.cli_set(src_path + ['rule', '1', 'protocol', proto]) +        self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_address]) +        self.cli_set(src_path + ['rule', '1', 'translation', 'port', tport]) + +        # check validate() - outbound-interface must be defined +        self.cli_commit() + +        nftables_search = [ +            ['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64 tcp dport { 9999 } tcp sport { 8080 } snat to 2001:db8:1111::1:80'] +        ] + +        self.verify_nftables(nftables_search, 'ip6 nat') +      def test_nat66_no_rules(self):          # T3206: deleting all rules but keep the direction 'destination' or          # 'source' resulteds in KeyError: 'rule'. diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index cefaad64a..6196ffe60 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -105,7 +105,8 @@ neighbor_config = {          'pfx_list_out'     : prefix_list_out6,          'no_send_comm_ext' : '',          'peer_group'       : 'foo-bar_baz', -        'graceful_rst_hlp' : '' +        'graceful_rst_hlp' : '', +        'disable_conn_chk' : '',          },  } @@ -120,6 +121,7 @@ peer_group_config = {          'shutdown'         : '',          'cap_over'         : '',          'ttl_security'     : '5', +        'disable_conn_chk' : '',          },      'bar' : {          'remote_as'        : '111', @@ -251,6 +253,9 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):              self.assertIn(f' neighbor {peer} graceful-restart-disable', frrconfig)          if 'graceful_rst_hlp' in peer_config:              self.assertIn(f' neighbor {peer} graceful-restart-helper', frrconfig) +        if 'disable_conn_chk' in peer_config: +            self.assertIn(f' neighbor {peer} disable-connected-check', frrconfig) +      def test_bgp_01_simple(self):          router_id = '127.0.0.1' @@ -400,6 +405,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):                  self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'disable'])              if 'graceful_rst_hlp' in peer_config:                  self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'restart-helper']) +            if 'disable_conn_chk' in peer_config: +                self.cli_set(base_path + ['neighbor', peer, 'disable-connected-check'])              # Conditional advertisement              if 'advertise_map' in peer_config: @@ -488,6 +495,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):                  self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'disable'])              if 'graceful_rst_hlp' in config:                  self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'restart-helper']) +            if 'disable_conn_chk' in config: +                self.cli_set(base_path + ['peer-group', peer_group, 'disable-connected-check'])              # Conditional advertisement              if 'advertise_map' in config: diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py index 1c8cc9759..c1c4044e6 100755 --- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -24,7 +24,7 @@ from vyos.util import process_named_running  from vyos.util import read_file  PROCESS_NAME = 'telegraf' -TELEGRAF_CONF = '/run/telegraf/vyos-telegraf.conf' +TELEGRAF_CONF = '/run/telegraf/telegraf.conf'  base_path = ['service', 'monitoring', 'telegraf']  org = 'log@in.local'  token = 'GuRJc12tIzfjnYdKRAIYbxdWd2aTpOT9PVYNddzDnFV4HkAcD7u7-kndTFXjGuXzJN6TTxmrvPODB4mnFcseDV==' diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 3057357fc..7cd7ea42e 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -29,6 +29,8 @@ from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key  from vyos.template import render  from vyos.util import call +from vyos.util import check_port_availability +from vyos.util import is_listen_port_bind_service  from vyos.util import write_file  from vyos import airbag @@ -107,6 +109,31 @@ def verify(https):                  raise ConfigError("At least one 'virtual-host <id> server-name' "                                "matching the 'certbot domain-name' is required.") +    server_block_list = [] + +    # organize by vhosts +    vhost_dict = https.get('virtual-host', {}) + +    if not vhost_dict: +        # no specified virtual hosts (server blocks); use default +        server_block_list.append(default_server_block) +    else: +        for vhost in list(vhost_dict): +            server_block = deepcopy(default_server_block) +            data = vhost_dict.get(vhost, {}) +            server_block['address'] = data.get('listen-address', '*') +            server_block['port'] = data.get('listen-port', '443') +            server_block_list.append(server_block) + +    for entry in server_block_list: +        _address = entry.get('address') +        _address = '0.0.0.0' if _address == '*' else _address +        _port = entry.get('port') +        proto = 'tcp' +        if check_port_availability(_address, int(_port), proto) is not True and \ +                not is_listen_port_bind_service(int(_port), 'nginx'): +            raise ConfigError(f'"{proto}" port "{_port}" is used by another service') +      verify_vrf(https)      return None diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 870049a88..649ea8d50 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -67,7 +67,7 @@ def get_config(config=None):          macsec.update({'shutdown_required': {}})      if 'source_interface' in macsec: -        tmp = is_source_interface(conf, macsec['source_interface'], 'macsec') +        tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet'])          if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp})      return macsec @@ -102,12 +102,6 @@ def verify(macsec):              # 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-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index f26a50a0e..20f2b1975 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.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 @@ -19,6 +19,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 @@ -51,6 +52,10 @@ def get_config(config=None):      if 'source_interface' in peth:          _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],                                                 peth['source_interface']) +        # test if source-interface is maybe already used by another interface +        tmp = is_source_interface(conf, peth['source_interface'], ['macsec']) +        if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp}) +      return peth  def verify(peth): diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 5490a794d..0ecb4d736 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -17,6 +17,7 @@  import os  from vyos.config import Config +from vyos.configdict import is_node_changed  from vyos.configverify import verify_vrf  from vyos.configverify import verify_interface_exists  from vyos.util import call @@ -40,6 +41,10 @@ def get_config(config=None):      ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)      ntp['config_file'] = config_file + +    tmp = is_node_changed(conf, base + ['vrf']) +    if tmp: ntp.update({'restart_required': {}}) +      return ntp  def verify(ntp): @@ -78,19 +83,25 @@ def generate(ntp):      return None  def apply(ntp): +    systemd_service = 'ntp.service' +    # Reload systemd manager configuration +    call('systemctl daemon-reload') +      if not ntp:          # NTP support is removed in the commit -        call('systemctl stop ntp.service') +        call(f'systemctl stop {systemd_service}')          if os.path.exists(config_file):              os.unlink(config_file)          if os.path.isfile(systemd_override):              os.unlink(systemd_override) +        return -    # Reload systemd manager configuration -    call('systemctl daemon-reload') -    if ntp: -        call('systemctl restart ntp.service') +    # we need to restart the service if e.g. the VRF name changed +    systemd_action = 'reload-or-restart' +    if 'restart_required' in ntp: +        systemd_action = 'restart' +    call(f'systemctl {systemd_action} {systemd_service}')      return None  if __name__ == '__main__': diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 62f5e1ddf..53df006a4 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -22,6 +22,8 @@ from shutil import rmtree  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf  from vyos.ifconfig import Section  from vyos.template import render  from vyos.util import call @@ -32,39 +34,14 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() - -base_dir = '/run/telegraf'  cache_dir = f'/etc/telegraf/.cache' -config_telegraf = f'{base_dir}/vyos-telegraf.conf' +config_telegraf = f'/run/telegraf/telegraf.conf'  custom_scripts_dir = '/etc/telegraf/custom_scripts'  syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf' -systemd_telegraf_service = '/etc/systemd/system/vyos-telegraf.service' -systemd_telegraf_override_dir = '/etc/systemd/system/vyos-telegraf.service.d' -systemd_override = f'{systemd_telegraf_override_dir}/10-override.conf' - - -def get_interfaces(type='', vlan=True): -    """ -    Get interfaces -    get_interfaces() -    ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] - -    get_interfaces("dummy") -    ['dum0'] -    """ -    interfaces = [] -    ifaces = Section.interfaces(type) -    for iface in ifaces: -        if vlan == False and '.' in iface: -            continue -        interfaces.append(iface) - -    return interfaces +systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf'  def get_nft_filter_chains(): -    """ -    Get nft chains for table filter -    """ +    """ Get nft chains for table filter """      nft = cmd('nft --json list table ip filter')      nft = json.loads(nft)      chain_list = [] @@ -76,9 +53,7 @@ def get_nft_filter_chains():      return chain_list -  def get_config(config=None): -      if config:          conf = config      else: @@ -87,8 +62,12 @@ def get_config(config=None):      if not conf.exists(base):          return None -    monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, -                                    no_tag_node_value_mangle=True) +    monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), +                                      get_first_key=True, +                                      no_tag_node_value_mangle=True) + +    tmp = is_node_changed(conf, base + ['vrf']) +    if tmp: monitoring.update({'restart_required': {}})      # We have gathered the dict representation of the CLI, but there are default      # options which we need to update into the dictionary retrived. @@ -96,7 +75,7 @@ def get_config(config=None):      monitoring = dict_merge(default_values, monitoring)      monitoring['custom_scripts_dir'] = custom_scripts_dir -    monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) +    monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)      monitoring['nft_chains'] = get_nft_filter_chains()      # Redefine azure group-metrics 'single-table' and 'table-per-metric' @@ -131,6 +110,8 @@ def verify(monitoring):      if not monitoring:          return None +    verify_vrf(monitoring) +      # Verify influxdb      if 'influxdb' in monitoring:          if 'authentication' not in monitoring['influxdb'] or \ @@ -173,7 +154,7 @@ def verify(monitoring):  def generate(monitoring):      if not monitoring:          # Delete config and systemd files -        config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf] +        config_files = [config_telegraf, systemd_override, syslog_telegraf]          for file in config_files:              if os.path.isfile(file):                  os.unlink(file) @@ -190,33 +171,34 @@ def generate(monitoring):      chown(cache_dir, 'telegraf', 'telegraf') -    # Create systemd override dir -    if not os.path.exists(systemd_telegraf_override_dir): -        os.mkdir(systemd_telegraf_override_dir) -      # Create custome scripts dir      if not os.path.exists(custom_scripts_dir):          os.mkdir(custom_scripts_dir)      # Render telegraf configuration and systemd override -    render(config_telegraf, 'monitoring/telegraf.j2', monitoring) -    render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.j2', monitoring) -    render(systemd_override, 'monitoring/override.conf.j2', monitoring, permission=0o640) -    render(syslog_telegraf, 'monitoring/syslog_telegraf.j2', monitoring) - -    chown(base_dir, 'telegraf', 'telegraf') +    render(config_telegraf, 'telegraf/telegraf.j2', monitoring, user='telegraf', group='telegraf') +    render(systemd_override, 'telegraf/override.conf.j2', monitoring) +    render(syslog_telegraf, 'telegraf/syslog_telegraf.j2', monitoring)      return None  def apply(monitoring):      # Reload systemd manager configuration +    systemd_service = 'telegraf.service'      call('systemctl daemon-reload') -    if monitoring: -        call('systemctl restart vyos-telegraf.service') -    else: -        call('systemctl stop vyos-telegraf.service') +    if not monitoring: +        call(f'systemctl stop {systemd_service}') +        return + +    # we need to restart the service if e.g. the VRF name changed +    systemd_action = 'reload-or-restart' +    if 'restart_required' in monitoring: +        systemd_action = 'restart' + +    call(f'systemctl {systemd_action} {systemd_service}') +      # Telegraf include custom rsyslog config changes -    call('systemctl restart rsyslog') +    call('systemctl reload-or-restart rsyslog')  if __name__ == '__main__':      try: diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 28669694b..2bbd7142a 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -22,6 +22,7 @@ from syslog import LOG_INFO  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed  from vyos.configverify import verify_vrf  from vyos.util import call  from vyos.template import render @@ -50,6 +51,10 @@ def get_config(config=None):          return None      ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + +    tmp = is_node_changed(conf, base + ['vrf']) +    if tmp: ssh.update({'restart_required': {}}) +      # 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) @@ -104,17 +109,25 @@ def generate(ssh):      return None  def apply(ssh): +    systemd_service_ssh = 'ssh.service' +    systemd_service_sshguard = 'sshguard.service'      if not ssh:          # SSH access is removed in the commit -        call('systemctl stop ssh.service') -        call('systemctl stop sshguard.service') +        call(f'systemctl stop {systemd_service_ssh}') +        call(f'systemctl stop {systemd_service_sshguard}')          return None +      if 'dynamic_protection' not in ssh: -        call('systemctl stop sshguard.service') +        call(f'systemctl stop {systemd_service_sshguard}')      else: -        call('systemctl restart sshguard.service') +        call(f'systemctl reload-or-restart {systemd_service_sshguard}') + +    # we need to restart the service if e.g. the VRF name changed +    systemd_action = 'reload-or-restart' +    if 'restart_required' in ssh: +        systemd_action = 'restart' -    call('systemctl restart ssh.service') +    call(f'systemctl {systemd_action} {systemd_service_ssh}')      return None  if __name__ == '__main__': diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index bad9cfbd8..5ca32d23e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -595,13 +595,11 @@ def wait_for_vici_socket(timeout=5, sleep_interval=0.1):          sleep(sleep_interval)  def apply(ipsec): +    systemd_service = 'strongswan-starter.service'      if not ipsec: -        call('sudo ipsec stop') +        call(f'systemctl stop {systemd_service}')      else: -        call('sudo ipsec restart') -        call('sudo ipsec rereadall') -        call('sudo ipsec reload') - +        call(f'systemctl reload-or-restart {systemd_service}')          if wait_for_vici_socket():              call('sudo swanctl -q') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 23e5162ba..2949ab290 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -26,7 +26,9 @@ from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key  from vyos.template import render  from vyos.util import call +from vyos.util import check_port_availability  from vyos.util import dict_search +from vyos.util import is_listen_port_bind_service  from vyos.util import write_file  from vyos import ConfigError  from vyos import airbag @@ -62,6 +64,12 @@ def verify(sstp):      if not sstp:          return None +    port = sstp.get('port') +    proto = 'tcp' +    if check_port_availability('0.0.0.0', int(port), proto) is not True and \ +            not is_listen_port_bind_service(int(port), 'accel-pppd'): +        raise ConfigError(f'"{proto}" port "{port}" is used by another service') +      verify_accel_ppp_base_service(sstp)      if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: diff --git a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py index 0c7474156..6f14d6a8e 100755 --- a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py +++ b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py @@ -5,20 +5,6 @@ from vyos.ifconfig import Interface  import time -def get_interfaces(type='', vlan=True): -    """ -    Get interfaces: -    ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] -    """ -    interfaces = [] -    ifaces = Section.interfaces(type) -    for iface in ifaces: -        if vlan == False and '.' in iface: -            continue -        interfaces.append(iface) - -    return interfaces -  def get_interface_addresses(iface, link_local_v6=False):      """      Get IP and IPv6 addresses from interface in one string @@ -77,7 +63,7 @@ def get_interface_oper_state(iface):      return oper_state -interfaces = get_interfaces() +interfaces = Section.interfaces('')  for iface in interfaces:      print(f'show_interfaces,interface={iface} ' diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index 036226418..b27aa6060 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -51,6 +51,21 @@ def _get_raw_data(family):      return _xml_to_dict(xml) +def _get_raw_statistics(): +    entries = [] +    data = cmd('sudo conntrack -S') +    data = data.replace('  \t', '').split('\n') +    for entry in data: +        entries.append(entry.split()) +    return entries + + +def get_formatted_statistics(entries): +    headers = ["CPU", "Found", "Invalid", "Insert", "Insert fail", "Drop", "Early drop", "Errors", "Search restart"] +    output = tabulate(entries, headers, numalign="left") +    return output + +  def get_formatted_output(dict_data):      """      :param xml: @@ -111,6 +126,14 @@ def show(raw: bool, family: str):          return get_formatted_output(conntrack_data) +def show_statistics(raw: bool): +    conntrack_statistics = _get_raw_statistics() +    if raw: +        return conntrack_statistics +    else: +        return get_formatted_statistics(conntrack_statistics) + +  if __name__ == '__main__':      try:          res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 49c8e6142..a4d1b4cb1 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -16,13 +16,122 @@  import re  import sys + +from collections import OrderedDict +from hurry import filesize +from re import split as re_split +from tabulate import tabulate +  from vyos.util import call +from vyos.util import convert_data +from vyos.util import seconds_to_human +  import vyos.opmode  SWANCTL_CONF = '/etc/swanctl/swanctl.conf' +def _convert(text): +    return int(text) if text.isdigit() else text.lower() + + +def _alphanum_key(key): +    return [_convert(c) for c in re_split('([0-9]+)', str(key))] + + +def _get_vici_sas(): +    from vici import Session as vici_session + +    session = vici_session() +    sas = list(session.list_sas()) +    return sas + + +def _get_raw_data_sas(): +    get_sas = _get_vici_sas() +    sas = convert_data(get_sas) +    return sas + + +def _get_formatted_output_sas(sas): +    sa_data = [] +    for sa in sas: +        for parent_sa in sa.values(): +            # create an item for each child-sa +            for child_sa in parent_sa.get('child-sas', {}).values(): +                # prepare a list for output data +                sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A' + +                # collect raw data +                sa_name = child_sa.get('name') +                sa_state = child_sa.get('state') +                sa_uptime = child_sa.get('install-time') +                sa_bytes_in = child_sa.get('bytes-in') +                sa_bytes_out = child_sa.get('bytes-out') +                sa_packets_in = child_sa.get('packets-in') +                sa_packets_out = child_sa.get('packets-out') +                sa_remote_addr = parent_sa.get('remote-host') +                sa_remote_id = parent_sa.get('remote-id') +                sa_proposal_encr_alg = child_sa.get('encr-alg') +                sa_proposal_integ_alg = child_sa.get('integ-alg') +                sa_proposal_encr_keysize = child_sa.get('encr-keysize') +                sa_proposal_dh_group = child_sa.get('dh-group') + +                # format data to display +                if sa_name: +                    sa_out_name = sa_name +                if sa_state: +                    if sa_state == 'INSTALLED': +                        sa_out_state = 'up' +                    else: +                        sa_out_state = 'down' +                if sa_uptime: +                    sa_out_uptime = seconds_to_human(sa_uptime) +                if sa_bytes_in and sa_bytes_out: +                    bytes_in = filesize.size(int(sa_bytes_in)) +                    bytes_out = filesize.size(int(sa_bytes_out)) +                    sa_out_bytes = f'{bytes_in}/{bytes_out}' +                if sa_packets_in and sa_packets_out: +                    packets_in = filesize.size(int(sa_packets_in), +                                               system=filesize.si) +                    packets_out = filesize.size(int(sa_packets_out), +                                                system=filesize.si) +                    packets_str = f'{packets_in}/{packets_out}' +                    sa_out_packets = re.sub(r'B', r'', packets_str) +                if sa_remote_addr: +                    sa_out_remote_addr = sa_remote_addr +                if sa_remote_id: +                    sa_out_remote_id = sa_remote_id +                # format proposal +                if sa_proposal_encr_alg: +                    sa_out_proposal = sa_proposal_encr_alg +                if sa_proposal_encr_keysize: +                    sa_proposal_encr_keysize_str = sa_proposal_encr_keysize +                    sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}' +                if sa_proposal_integ_alg: +                    sa_proposal_integ_alg_str = sa_proposal_integ_alg +                    sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}' +                if sa_proposal_dh_group: +                    sa_proposal_dh_group_str = sa_proposal_dh_group +                    sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}' + +                # add a new item to output data +                sa_data.append([ +                    sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes, +                    sa_out_packets, sa_out_remote_addr, sa_out_remote_id, +                    sa_out_proposal +                ]) + +    headers = [ +        "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", +        "Remote address", "Remote ID", "Proposal" +    ] +    sa_data = sorted(sa_data, key=_alphanum_key) +    output = tabulate(sa_data, headers) +    return output + +  def get_peer_connections(peer, tunnel, return_all = False):      peer = peer.replace(':', '-')      search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' @@ -61,6 +170,13 @@ def reset_peer(peer: str, tunnel:str):      print('Peer reset result: ' + ('success' if result else 'failed')) +def show_sa(raw: bool): +    sa_data = _get_raw_data_sas() +    if raw: +        return sa_data +    return _get_formatted_output_sas(sa_data) + +  if __name__ == '__main__':      try:          res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 12fc4c782..1339d5b92 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -17,6 +17,7 @@  import jmespath  import json  import sys +import xmltodict  from sys import exit  from tabulate import tabulate @@ -27,7 +28,30 @@ from vyos.util import dict_search  import vyos.opmode -def _get_json_data(direction): +def _get_xml_translation(direction, family): +    """ +    Get conntrack XML output --src-nat|--dst-nat +    """ +    if direction == 'source': +        opt = '--src-nat' +    if direction == 'destination': +        opt = '--dst-nat' +    return cmd(f'sudo conntrack --dump --family {family} {opt} --output xml') + + +def _xml_to_dict(xml): +    """ +    Convert XML to dictionary +    Return: dictionary +    """ +    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_json_data(direction, family):      """      Get NAT format JSON      """ @@ -35,14 +59,15 @@ def _get_json_data(direction):          chain = 'POSTROUTING'      if direction == 'destination':          chain = 'PREROUTING' -    return cmd(f'sudo nft --json list chain ip nat {chain}') +    family = 'ip6' if family == 'inet6' else 'ip' +    return cmd(f'sudo nft --json list chain {family} nat {chain}') -def _get_raw_data_rules(direction): +def _get_raw_data_rules(direction, family):      """Get interested rules      :returns dict      """ -    data = _get_json_data(direction) +    data = _get_json_data(direction, family)      data_dict = json.loads(data)      rules = []      for rule in data_dict['nftables']: @@ -51,10 +76,28 @@ def _get_raw_data_rules(direction):      return rules -def _get_formatted_output_rules(data, direction): +def _get_raw_translation(direction, family): +    """ +    Return: dictionary +    """ +    xml = _get_xml_translation(direction, family) +    if len(xml) == 0: +        output = {'conntrack': +            { +                'error': True, +                'reason': 'entries not found' +            } +        } +        return output +    return _xml_to_dict(xml) + + +def _get_formatted_output_rules(data, direction, family):      # Add default values before loop      sport, dport, proto = 'any', 'any', 'any' -    saddr, daddr = '0.0.0.0/0', '0.0.0.0/0' +    saddr = '::/0' if family == 'inet6' else '0.0.0.0/0' +    daddr = '::/0' if family == 'inet6' else '0.0.0.0/0' +      data_entries = []      for rule in data:          if 'comment' in rule['rule']: @@ -69,11 +112,13 @@ def _get_formatted_output_rules(data, direction):                  if 'prefix' in match['right'] or 'set' in match['right']:                      # Merge dict src/dst l3_l4 parameters                      my_dict = {**match['left']['payload'], **match['right']} +                    my_dict['op'] = match['op'] +                    op = '!' if my_dict.get('op') == '!=' else ''                      proto = my_dict.get('protocol').upper()                      if my_dict['field'] == 'saddr': -                        saddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' +                        saddr = f'{op}{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}'                      elif my_dict['field'] == 'daddr': -                        daddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' +                        daddr = f'{op}{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): @@ -96,8 +141,8 @@ def _get_formatted_output_rules(data, direction):                      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' +                saddr = '::/0' if family == 'inet6' else '0.0.0.0/0' +                daddr = '::/0' if family == 'inet6' else '0.0.0.0/0'                  sport = 'any'                  dport = 'any'                  proto = 'any' @@ -175,22 +220,83 @@ def _get_formatted_output_statistics(data, direction):      return output -def show_rules(raw: bool, direction: str): -    nat_rules = _get_raw_data_rules(direction) +def _get_formatted_translation(dict_data, nat_direction, family): +    data_entries = [] +    if 'error' in dict_data['conntrack']: +        return 'Entries not found' +    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'] +            if direction in ['original']: +                if 'layer3' in meta: +                    orig_src = meta['layer3']['src'] +                    orig_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        orig_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        orig_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['protoname'] +            if direction in ['reply']: +                if 'layer3' in meta: +                    reply_src = meta['layer3']['src'] +                    reply_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        reply_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        reply_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['protoname'] +            if direction == 'independent': +                conn_id = meta['id'] +                timeout = meta['timeout'] +                orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src +                orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst +                reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src +                reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst +                state = meta['state'] if 'state' in meta else '' +                mark = meta['mark'] +                zone = meta['zone'] if 'zone' in meta else '' +                if nat_direction == 'source': +                    data_entries.append( +                        [orig_src, reply_dst, proto, timeout, mark, zone]) +                elif nat_direction == 'destination': +                    data_entries.append( +                        [orig_dst, reply_src, proto, timeout, mark, zone]) + +    headers = ["Pre-NAT", "Post-NAT", "Proto", "Timeout", "Mark", "Zone"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def show_rules(raw: bool, direction: str, family: str): +    nat_rules = _get_raw_data_rules(direction, family)      if raw:          return nat_rules      else: -        return _get_formatted_output_rules(nat_rules, direction) +        return _get_formatted_output_rules(nat_rules, direction, family) -def show_statistics(raw: bool, direction: str): -    nat_statistics = _get_raw_data_rules(direction) +def show_statistics(raw: bool, direction: str, family: str): +    nat_statistics = _get_raw_data_rules(direction, family)      if raw:          return nat_statistics      else:          return _get_formatted_output_statistics(nat_statistics, direction) +def show_translations(raw: bool, direction: str, family: str): +    family = 'ipv6' if family == 'inet6' else 'ipv4' +    nat_translation = _get_raw_translation(direction, family) +    if raw: +        return nat_translation +    else: +        return _get_formatted_translation(nat_translation, direction, family) + +  if __name__ == '__main__':      try:          res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/show_nat66_rules.py b/src/op_mode/show_nat66_rules.py deleted file mode 100755 index 967ec9d37..000000000 --- a/src/op_mode/show_nat66_rules.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program.  If not, see <http://www.gnu.org/licenses/>. - -import 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 ip6 nat') -    tmp = json.loads(tmp) -     -    format_nat66_rule = '{0: <10} {1: <50} {2: <50} {3: <10}' -    print(format_nat66_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) -    print(format_nat66_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-NAT66-', 'DST-NAT66-']: -            if comment_prefix in comment: -                continue_rule = False -        if continue_rule: -            continue -         -        # When log is detected from the second index of expr, then this rule should be ignored -        if 'log' in data['expr'][2]: -            continue -         -        rule = comment.replace('SRC-NAT66-','') -        rule = rule.replace('DST-NAT66-','') -        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 = dict_search('match.right.prefix.addr', data['expr'][2]) -        if srcdest: -            addr_tmp = dict_search('match.right.prefix.len', data['expr'][2]) -            if addr_tmp: -                srcdest = srcdest + '/' + str(addr_tmp) -        else: -            srcdest = dict_search('match.right', data['expr'][2]) -         -        tran_addr_json = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3]) -        if tran_addr_json: -            if isinstance(srcdest_json,str): -                tran_addr = tran_addr_json - -            if 'prefix' in tran_addr_json: -                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: -                    tran_addr = addr_tmp + '/' + str(len_tmp) -        else: -            if 'masquerade' in data['expr'][3]: -                tran_addr = 'masquerade' -         -        print(format_nat66_rule.format(rule, srcdest, tran_addr, interface)) -     -    exit(0) -else: -    parser.print_help() -    exit(1) - diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 3e89fb239..c8ae0f516 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -21,7 +21,7 @@ from makefun import with_signature  from .. import state  from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session  mutation = ObjectType("Mutation") @@ -71,7 +71,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func):              # one may override the session functions with a local subclass              try: -                mod = import_module(f'api.graphql.recipes.{func_base_name}') +                mod = import_module(f'api.graphql.session.override.{func_base_name}')                  klass = getattr(mod, class_name)              except ImportError:                  # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index f6544709e..921a66274 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -21,7 +21,7 @@ from makefun import with_signature  from .. import state  from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session  query = ObjectType("Query") @@ -71,7 +71,7 @@ def make_query_resolver(query_name, class_name, session_func):              # one may override the session functions with a local subclass              try: -                mod = import_module(f'api.graphql.recipes.{func_base_name}') +                mod = import_module(f'api.graphql.session.override.{func_base_name}')                  klass = getattr(mod, class_name)              except ImportError:                  # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/session/__init__.py index e69de29bb..e69de29bb 100644 --- a/src/services/api/graphql/recipes/__init__.py +++ b/src/services/api/graphql/session/__init__.py diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/session/composite/system_status.py index 8dadcc9f3..8dadcc9f3 100755 --- a/src/services/api/graphql/recipes/queries/system_status.py +++ b/src/services/api/graphql/session/composite/system_status.py diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py index b91932e14..b91932e14 100644 --- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py +++ b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/session/session.py index ac185beb7..23bc7154c 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/session/session.py @@ -149,7 +149,7 @@ class Session:          return res      def system_status(self): -        import api.graphql.recipes.queries.system_status as system_status +        import api.graphql.session.composite.system_status as system_status          session = self._session          data = self._data diff --git a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl index 70de43183..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl +++ b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl index a890d0086..a890d0086 100644 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl +++ b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl index e9b660722..e9b660722 100644 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl +++ b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl index d9d7ed691..d9d7ed691 100644 --- a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl +++ b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl index 458f3e5fc..458f3e5fc 100644 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl +++ b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl index 0efa0b226..0efa0b226 100644 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl +++ b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl index f56c61231..f56c61231 100644 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl +++ b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl index f98a5517c..f98a5517c 100644 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl +++ b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py index d27586747..f990aae52 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -25,7 +25,10 @@ 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 +if __package__ is None or __package__ == '': +    from util import load_as_module, is_op_mode_function_name, is_show_function_name +else: +    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'] diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index af8837e1e..190f3409d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -678,6 +678,7 @@ if __name__ == '__main__':          server_config = load_server_config()      except Exception as err:          logger.critical(f"Failed to load the HTTP API server config: {err}") +        sys.exit(1)      config_session = ConfigSession(os.getpid()) diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index a8df232ae..a0fccd1d0 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -30,6 +30,7 @@ from vyos.ifconfig.vrrp import VRRP  from vyos.configquery import ConfigTreeQuery  from vyos.util import cmd  from vyos.util import dict_search +from vyos.util import commit_in_progress  # configure logging  logger = logging.getLogger(__name__) @@ -63,6 +64,17 @@ class KeepalivedFifo:      # load configuration      def _config_load(self): +        # For VRRP configuration to be read, the commit must be finished +        count = 1 +        while commit_in_progress(): +            if ( count <= 40 ): +                logger.debug(f'commit in progress try: {count}') +            else: +                logger.error(f'commit still in progress after {count} continuing anyway') +                break +            count += 1 +            time.sleep(0.5) +          try:              base = ['high-availability', 'vrrp']              conf = ConfigTreeQuery() diff --git a/data/templates/monitoring/systemd_vyos_telegraf_service.j2 b/src/systemd/telegraf.service index 234ef5586..553942ac6 100644 --- a/data/templates/monitoring/systemd_vyos_telegraf_service.j2 +++ b/src/systemd/telegraf.service @@ -5,8 +5,7 @@ After=network.target  [Service]  EnvironmentFile=-/etc/default/telegraf -User=telegraf -ExecStart=/usr/bin/telegraf -config /run/telegraf/vyos-telegraf.conf -config-directory /etc/telegraf/telegraf.d $TELEGRAF_OPTS +ExecStart=/usr/bin/telegraf --config /run/telegraf/vyos-telegraf.conf --config-directory /etc/telegraf/telegraf.d  ExecReload=/bin/kill -HUP $MAINPID  Restart=on-failure  RestartForceExitStatus=SIGPIPE | 
