diff options
29 files changed, 745 insertions, 91 deletions
diff --git a/.github/reviewers.yml b/.github/reviewers.yml index 8463681fc..a6f0a3785 100644 --- a/.github/reviewers.yml +++ b/.github/reviewers.yml @@ -1,7 +1,7 @@ --- "**/*": - dmbaturin - - UnicronNL + - sarthurdev - zdc - jestabro - sever-sever diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index 81134206b..a769145f8 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Request review based on files changes and/or groups the author belongs to - uses: shufo/auto-assign-reviewer-by-files@v1.1.1 + uses: shufo/auto-assign-reviewer-by-files@v1.1.4 with: token: ${{ secrets.GITHUB_TOKEN }} config: .github/reviewers.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..c39800ac8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "current", crux, equuleus ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "current" ] + schedule: + - cron: '22 10 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # âšī¸ Command-line programs to run using the OS shell. + # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index ec950765d..b162f4097 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -1,4 +1,5 @@ [ +"accelppp.py", "bgp.py", "bridge.py", "conntrack.py", diff --git a/data/templates/dns-forwarding/recursor.conf.j2 b/data/templates/dns-forwarding/recursor.conf.j2 index ce1b676d1..e02e6c13d 100644 --- a/data/templates/dns-forwarding/recursor.conf.j2 +++ b/data/templates/dns-forwarding/recursor.conf.j2 @@ -29,6 +29,9 @@ export-etc-hosts={{ 'no' if ignore_hosts_file is vyos_defined else 'yes' }} # listen-address local-address={{ listen_address | join(',') }} +# listen-port +local-port={{ port }} + # dnssec dnssec={{ dnssec }} diff --git a/data/templates/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2 index 5ad4bd28c..9b5e80aed 100644 --- a/data/templates/frr/policy.frr.j2 +++ b/data/templates/frr/policy.frr.j2 @@ -322,6 +322,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.set.ipv6_next_hop.prefer_global is vyos_defined %} set ipv6 next-hop prefer-global {% endif %} +{% if rule_config.set.l3vpn_nexthop.encapsulation.gre is vyos_defined %} +set l3vpn next-hop encapsulation gre +{% endif %} {% if rule_config.set.large_community.replace is vyos_defined %} set large-community {{ rule_config.set.large_community.replace | join(' ') }} {% endif %} diff --git a/debian/vyos-1x-smoketest.postinst b/debian/vyos-1x-smoketest.postinst index b33376bc3..18612804c 100755 --- a/debian/vyos-1x-smoketest.postinst +++ b/debian/vyos-1x-smoketest.postinst @@ -1,6 +1,10 @@ #!/bin/sh -e BUSYBOX_TAG="docker.io/library/busybox:stable" -OUTPUT_PATH="docker-archive://usr/share/vyos/busybox-stable.tar" +OUTPUT_PATH="/usr/share/vyos/busybox-stable.tar" -skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "$OUTPUT_PATH" +if [[ -f $OUTPUT_PATH ]]; then + rm -f $OUTPUT_PATH +fi + +skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$OUTPUT_PATH" diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index d92fd8233..d5f5cbbc7 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -103,7 +103,8 @@ DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/ /etc/default/pmacctd /etc/pmacct /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf /etc/ntp.conf /etc/default/ssh - /etc/powerdns /etc/default/pdns-recursor" + /etc/powerdns /etc/default/pdns-recursor + /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns" for tmp in $DELETE; do if [ -e ${tmp} ]; then rm -rf ${tmp} diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 3de0dc0eb..409028572 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -605,6 +605,10 @@ </properties> </leafNode> #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>53</defaultValue> + </leafNode> <leafNode name="negative-ttl"> <properties> <help>Maximum amount of time negative entries are cached</help> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index 6c60276d5..b3745fda0 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -1356,6 +1356,26 @@ </leafNode> </children> </node> + <node name="l3vpn-nexthop"> + <properties> + <help>Next hop Information</help> + </properties> + <children> + <node name="encapsulation"> + <properties> + <help>Encapsulation options (for BGP only)</help> + </properties> + <children> + <leafNode name="gre"> + <properties> + <help>Accept L3VPN traffic over GRE encapsulation</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> <leafNode name="local-preference"> <properties> <help>BGP local preference attribute</help> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 01462ad8f..dccdfaf9a 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -118,7 +118,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> </completionHelp> </properties> - <command>journalctl --no-hostname --boot --follow --unit "ppp@$6.service"</command> + <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 8906d9ef3..404de1913 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -267,7 +267,7 @@ <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> </completionHelp> </properties> - <command>journalctl --no-hostname --boot --unit "ppp@$6.service"</command> + <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> </tagNode> </children> </node> diff --git a/python/vyos/accel_ppp.py b/python/vyos/accel_ppp.py new file mode 100644 index 000000000..bfc8ee5a9 --- /dev/null +++ b/python/vyos/accel_ppp.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys + +import vyos.opmode +from vyos.util import rc_cmd + + +def get_server_statistics(accel_statistics, pattern, sep=':') -> dict: + import re + + stat_dict = {'sessions': {}} + + cpu = re.search(r'cpu(.*)', accel_statistics).group(0) + # Find all lines with pattern, for example 'sstp:' + data = re.search(rf'{pattern}(.*)', accel_statistics, re.DOTALL).group(0) + session_starting = re.search(r'starting(.*)', data).group(0) + session_active = re.search(r'active(.*)', data).group(0) + + for entry in {cpu, session_starting, session_active}: + if sep in entry: + key, value = entry.split(sep) + if key in ['starting', 'active', 'finishing']: + stat_dict['sessions'][key] = value.strip() + continue + stat_dict[key] = value.strip() + return stat_dict + + +def accel_cmd(port: int, command: str) -> str: + _, output = rc_cmd(f'/usr/bin/accel-cmd -p{port} {command}') + return output + + +def accel_out_parse(accel_output: list[str]) -> list[dict[str, str]]: + """ Parse accel-cmd show sessions output """ + data_list: list[dict[str, str]] = list() + field_names: list[str] = list() + + field_names_unstripped: list[str] = accel_output.pop(0).split('|') + for field_name in field_names_unstripped: + field_names.append(field_name.strip()) + + while accel_output: + if '|' not in accel_output[0]: + accel_output.pop(0) + continue + + current_item: list[str] = accel_output.pop(0).split('|') + item_dict: dict[str, str] = {} + + for field_index in range(len(current_item)): + field_name: str = field_names[field_index] + field_value: str = current_item[field_index].strip() + item_dict[field_name] = field_value + + data_list.append(item_dict) + + return data_list diff --git a/python/vyos/base.py b/python/vyos/base.py index 78067d5b2..9b93cb2f2 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -15,17 +15,47 @@ from textwrap import fill + +class BaseWarning: + def __init__(self, header, message, **kwargs): + self.message = message + self.kwargs = kwargs + if 'width' not in kwargs: + self.width = 72 + if 'initial_indent' in kwargs: + del self.kwargs['initial_indent'] + if 'subsequent_indent' in kwargs: + del self.kwargs['subsequent_indent'] + self.textinitindent = header + self.standardindent = '' + + def print(self): + messages = self.message.split('\n') + isfirstmessage = True + initial_indent = self.textinitindent + print('') + for mes in messages: + mes = fill(mes, initial_indent=initial_indent, + subsequent_indent=self.standardindent, **self.kwargs) + if isfirstmessage: + isfirstmessage = False + initial_indent = self.standardindent + print(f'{mes}') + print('') + + class Warning(): - def __init__(self, message): - # Reformat the message and trim it to 72 characters in length - message = fill(message, width=72) - print(f'\nWARNING: {message}') + def __init__(self, message, **kwargs): + self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs) + self.BaseWarn.print() + class DeprecationWarning(): - def __init__(self, message): + def __init__(self, message, **kwargs): # Reformat the message and trim it to 72 characters in length - message = fill(message, width=72) - print(f'\nDEPRECATION WARNING: {message}\n') + self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs) + self.BaseWarn.print() + class ConfigError(Exception): def __init__(self, message): diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index e9cdb69e4..b88615513 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -1,5 +1,5 @@ # configtree -- a standalone VyOS config file manipulation library (Python bindings) -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or modify it under the terms of # the GNU Lesser General Public License as published by the Free Software Foundation; @@ -12,6 +12,7 @@ # You should have received a copy of the GNU Lesser General Public License along with this library; # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import os import re import json @@ -147,6 +148,8 @@ class ConfigTree(object): self.__config = address self.__version = '' + self.__migration = os.environ.get('VYOS_MIGRATION') + def __del__(self): if self.__config is not None: self.__destroy(self.__config) @@ -191,18 +194,27 @@ class ConfigTree(object): else: self.__set_add_value(self.__config, path_str, str(value).encode()) + if self.__migration: + print(f"- op: set path: {path} value: {value} replace: {replace}") + def delete(self, path): check_path(path) path_str = " ".join(map(str, path)).encode() self.__delete(self.__config, path_str) + if self.__migration: + print(f"- op: delete path: {path}") + def delete_value(self, path, value): check_path(path) path_str = " ".join(map(str, path)).encode() self.__delete_value(self.__config, path_str, value.encode()) + if self.__migration: + print(f"- op: delete_value path: {path} value: {value}") + def rename(self, path, new_name): check_path(path) path_str = " ".join(map(str, path)).encode() @@ -216,6 +228,9 @@ class ConfigTree(object): if (res != 0): raise ConfigTreeError("Path [{}] doesn't exist".format(path)) + if self.__migration: + print(f"- op: rename old_path: {path} new_path: {new_path}") + def copy(self, old_path, new_path): check_path(old_path) check_path(new_path) @@ -229,6 +244,9 @@ class ConfigTree(object): if (res != 0): raise ConfigTreeError("Path [{}] doesn't exist".format(old_path)) + if self.__migration: + print(f"- op: copy old_path: {old_path} new_path: {new_path}") + def exists(self, path): check_path(path) path_str = " ".join(map(str, path)).encode() diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py index 45ea8b0eb..87c74e1ea 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -16,9 +16,13 @@ import sys import os import json -import subprocess +import logging + import vyos.defaults import vyos.component_version as component_version +from vyos.util import cmd + +log_file = os.path.join(vyos.defaults.directories['config'], 'vyos-migrate.log') class MigratorError(Exception): pass @@ -29,9 +33,21 @@ class Migrator(object): self._force = force self._set_vintage = set_vintage self._config_file_vintage = None - self._log_file = None self._changed = False + def init_logger(self): + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + + # on adding the file handler, allow write permission for cfg_group; + # restore original umask on exit + mask = os.umask(0o113) + fh = logging.FileHandler(log_file) + formatter = logging.Formatter('%(message)s') + fh.setFormatter(formatter) + self.logger.addHandler(fh) + os.umask(mask) + def read_config_file_versions(self): """ Get component versions from config file footer and set vintage; @@ -68,34 +84,15 @@ class Migrator(object): else: return True - def open_log_file(self): - """ - Open log file for migration, catching any error. - Note that, on boot, migration takes place before the canonical log - directory is created, hence write to the config file directory. - """ - self._log_file = os.path.join(vyos.defaults.directories['config'], - 'vyos-migrate.log') - # on creation, allow write permission for cfg_group; - # restore original umask on exit - mask = os.umask(0o113) - try: - log = open('{0}'.format(self._log_file), 'w') - log.write("List of executed migration scripts:\n") - except Exception as e: - os.umask(mask) - print("Logging error: {0}".format(e)) - return None - - os.umask(mask) - return log - def run_migration_scripts(self, config_file_versions, system_versions): """ Run migration scripts iteratively, until config file version equals system component version. """ - log = self.open_log_file() + os.environ['VYOS_MIGRATION'] = '1' + self.init_logger() + + self.logger.info("List of executed migration scripts:") cfg_versions = config_file_versions sys_versions = system_versions @@ -127,8 +124,9 @@ class Migrator(object): '{}-to-{}'.format(cfg_ver, next_ver)) try: - subprocess.check_call([migrate_script, - self._config_file]) + out = cmd([migrate_script, self._config_file]) + self.logger.info(f'{migrate_script}') + if out: self.logger.info(out) except FileNotFoundError: pass except Exception as err: @@ -136,19 +134,10 @@ class Migrator(object): "".format(migrate_script, err)) sys.exit(1) - if log: - try: - log.write('{0}\n'.format(migrate_script)) - except Exception as e: - print("Error writing log: {0}".format(e)) - cfg_ver = next_ver - rev_versions[key] = cfg_ver - if log: - log.close() - + del os.environ['VYOS_MIGRATION'] return rev_versions def write_config_file_versions(self, cfg_versions): diff --git a/smoketest/configs/basic-qos b/smoketest/configs/basic-qos new file mode 100644 index 000000000..d9baa4a1f --- /dev/null +++ b/smoketest/configs/basic-qos @@ -0,0 +1,194 @@ +interfaces { + ethernet eth0 { + address 100.64.0.1/20 + duplex auto + smp-affinity auto + speed auto + } + ethernet eth1 { + duplex auto + speed auto + vif 10 { + traffic-policy { + in M2 + } + } + vif 20 { + traffic-policy { + out FS + } + } + vif 30 { + traffic-policy { + out MY-HTB + } + } + vif 40 { + traffic-policy { + out SHAPER-FOO + } + } + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + name-server 192.168.0.1 + syslog { + global { + archive { + file 5 + size 512 + } + facility all { + level info + } + } + } + time-zone Europe/Berlin +} +traffic-policy { + limiter M2 { + class 10 { + bandwidth 120mbit + burst 15k + match ADDRESS10 { + ip { + dscp CS4 + } + } + priority 20 + } + default { + bandwidth 100mbit + burst 15k + } + } + shaper FS { + bandwidth auto + class 10 { + bandwidth 100% + burst 15k + match ADDRESS10 { + ip { + source { + address 172.17.1.2/32 + } + } + } + queue-type fair-queue + set-dscp CS4 + } + class 20 { + bandwidth 100% + burst 15k + match ADDRESS20 { + ip { + source { + address 172.17.1.3/32 + } + } + } + queue-type fair-queue + set-dscp CS5 + } + class 30 { + bandwidth 100% + burst 15k + match ADDRESS30 { + ip { + source { + address 172.17.1.4/32 + } + } + } + queue-type fair-queue + set-dscp CS6 + } + default { + bandwidth 10% + burst 15k + ceiling 100% + priority 7 + queue-type fair-queue + } + } + shaper MY-HTB { + bandwidth 10mbit + class 30 { + bandwidth 10% + burst 15k + ceiling 50% + match ADDRESS30 { + ip { + source { + address 10.1.1.0/24 + } + } + } + priority 5 + queue-type fair-queue + } + class 40 { + bandwidth 90% + burst 15k + ceiling 100% + match ADDRESS40 { + ip { + dscp CS4 + source { + address 10.2.1.0/24 + } + } + } + priority 5 + queue-type fair-queue + } + class 50 { + bandwidth 100% + burst 15k + match ADDRESS50 { + ip { + dscp CS5 + } + } + queue-type fair-queue + set-dscp CS7 + } + default { + bandwidth 10% + burst 15k + ceiling 100% + priority 7 + queue-type fair-queue + set-dscp CS1 + } + } + shaper SHAPER-FOO { + bandwidth 1000mbit + default { + bandwidth 100mbit + burst 15k + queue-type fair-queue + set-dscp CS4 + } + } +} +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.2 diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 2166e63ec..3a4ef666a 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1030,6 +1030,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'metric' : '150', 'metric-type' : 'type-1', 'origin' : 'incomplete', + 'l3vpn' : '', 'originator-id' : '172.16.10.1', 'src' : '100.0.0.1', 'tag' : '65530', @@ -1229,6 +1230,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'set', 'ipv6-next-hop', 'local', rule_config['set']['ipv6-next-hop-local']]) if 'ip-next-hop' in rule_config['set']: self.cli_set(path + ['rule', rule, 'set', 'ip-next-hop', rule_config['set']['ip-next-hop']]) + if 'l3vpn' in rule_config['set']: + self.cli_set(path + ['rule', rule, 'set', 'l3vpn-nexthop', 'encapsulation', 'gre']) if 'local-preference' in rule_config['set']: self.cli_set(path + ['rule', rule, 'set', 'local-preference', rule_config['set']['local-preference']]) if 'metric' in rule_config['set']: @@ -1408,6 +1411,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): tmp += 'ipv6 next-hop global ' + rule_config['set']['ipv6-next-hop-global'] elif 'ipv6-next-hop-local' in rule_config['set']: tmp += 'ipv6 next-hop local ' + rule_config['set']['ipv6-next-hop-local'] + elif 'l3vpn' in rule_config['set']: + tmp += 'l3vpn next-hop encapsulation gre' elif 'local-preference' in rule_config['set']: tmp += 'local-preference ' + rule_config['set']['local-preference'] elif 'metric' in rule_config['set']: diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index fe2682d50..94e0597ad 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -111,6 +111,10 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('serve-rfc1918') self.assertEqual(tmp, 'yes') + # verify default port configuration + tmp = get_config_value('local-port') + self.assertEqual(tmp, '53') + def test_dnssec(self): # DNSSEC option testing @@ -224,5 +228,21 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('dns64-prefix') self.assertEqual(tmp, dns_prefix) + def test_listening_port(self): + # We can listen on a different port compared to '53' but only one at a time + for port in ['1053', '5353']: + self.cli_set(base_path + ['port', port]) + for network in allow_from: + self.cli_set(base_path + ['allow-from', network]) + for address in listen_adress: + self.cli_set(base_path + ['listen-address', address]) + + # commit changes + self.cli_commit() + + # verify local-port configuration + tmp = get_config_value('local-port') + self.assertEqual(tmp, port) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index b1902b585..518abeaec 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -33,8 +33,8 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then if [ -n "$new_dhcp6_name_servers" ]; then logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" - logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" + logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface" hostsd_changes=y fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ad6a1d5eb..da1bda137 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -8,7 +8,7 @@ hostsd_changes= /usr/bin/systemctl -q is-active vyos-hostsd hostsd_status=$? -if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then +if [[ $reason =~ ^(EXPIRE|FAIL|RELEASE|STOP)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" @@ -96,7 +96,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then fi fi -if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then +if [[ $reason =~ ^(EXPIRE6|RELEASE6|STOP6)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook index eeb8b0782..49bb18372 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook @@ -8,12 +8,12 @@ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # 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. -# +# # This code was originally developed by Vyatta, Inc. # Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc. # All Rights Reserved. @@ -23,7 +23,7 @@ RUN="yes" proto="" -if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then +if [[ $reason =~ ^(REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6)$ ]]; then proto="v6" fi diff --git a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers new file mode 100755 index 000000000..222c75f21 --- /dev/null +++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers @@ -0,0 +1,15 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" +$hostsd_client --delete-name-servers --tag "dhcp-$interface" +$hostsd_client --apply diff --git a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers new file mode 100755 index 000000000..0fcedbedc --- /dev/null +++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers @@ -0,0 +1,24 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" + +$hostsd_client --delete-name-servers --tag "dhcp-$interface" + +if [ "$USEPEERDNS" ] && [ -n "$DNS1" ]; then +$hostsd_client --add-name-servers "$DNS1" --tag "dhcp-$interface" +fi +if [ "$USEPEERDNS" ] && [ -n "$DNS2" ]; then +$hostsd_client --add-name-servers "$DNS2" --tag "dhcp-$interface" +fi + +$hostsd_client --apply diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py new file mode 100755 index 000000000..2fd045dc3 --- /dev/null +++ b/src/op_mode/accelppp.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys + +import vyos.accel_ppp +import vyos.opmode + +from vyos.configquery import ConfigTreeQuery +from vyos.util import rc_cmd + + +accel_dict = { + 'ipoe': { + 'port': 2002, + 'path': 'service ipoe-server' + }, + 'pppoe': { + 'port': 2001, + 'path': 'service pppoe-server' + }, + 'pptp': { + 'port': 2003, + 'path': 'vpn pptp' + }, + 'l2tp': { + 'port': 2004, + 'path': 'vpn l2tp' + }, + 'sstp': { + 'port': 2005, + 'path': 'vpn sstp' + } +} + + +def _get_raw_statistics(accel_output, pattern): + return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':') + + +def _get_raw_sessions(port): + cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \ + 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \ + 'tx-bytes-raw,rx-pkts,tx-pkts' + output = vyos.accel_ppp.accel_cmd(port, cmd_options) + parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse( + output.splitlines()) + return parsed_data + + +def _verify(func): + """Decorator checks if accel-ppp protocol + ipoe/pppoe/pptp/l2tp/sstp is configured + + for example: + service ipoe-server + vpn sstp + """ + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + protocol_list = accel_dict.keys() + protocol = kwargs.get('protocol') + # unknown or incorrect protocol query + if protocol not in protocol_list: + unconf_message = f'unknown protocol "{protocol}"' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + # Check if config does not exist + config_protocol_path = accel_dict[protocol]['path'] + if not config.exists(config_protocol_path): + unconf_message = f'"{config_protocol_path}" is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def show_statistics(raw: bool, protocol: str): + """show accel-cmd statistics + CPU utilization and amount of sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + pattern = f'{protocol}:' + port = accel_dict[protocol]['port'] + rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat') + + if raw: + return _get_raw_statistics(output, pattern) + + return output + + +@_verify +def show_sessions(raw: bool, protocol: str): + """show accel-cmd sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + port = accel_dict[protocol]['port'] + if raw: + return _get_raw_sessions(port) + + return vyos.accel_ppp.accel_cmd(port, + 'show sessions ifname,username,ip,ip6,ip6-dp,' + 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes') + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py index 5a821a287..d6098c158 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -32,7 +32,7 @@ def _get_json_data(): """ Get bridge data format JSON """ - return cmd(f'sudo bridge --json link show') + return cmd(f'bridge --json link show') def _get_raw_data_summary(): @@ -48,7 +48,7 @@ def _get_raw_data_vlan(): """ :returns dict """ - json_data = cmd('sudo bridge --json --compressvlans vlan show') + json_data = cmd('bridge --json --compressvlans vlan show') data_dict = json.loads(json_data) return data_dict @@ -57,7 +57,7 @@ def _get_raw_data_fdb(bridge): """Get MAC-address for the bridge brX :returns list """ - code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}') + code, json_data = rc_cmd(f'bridge --json fdb show br {bridge}') # From iproute2 fdb.c, fdb_show() will only exit(-1) in case of # non-existent bridge device; raise error. if code == 255: diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 950feb625..46bda5f7e 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -63,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True): get_first_key=True, no_tag_node_value_mangle=True) if firewall and interfaces: if name: - firewall['interface'] = [] + firewall['interface'] = {} else: if 'name' in firewall: for fw_name, name_conf in firewall['name'].items(): diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index 5e5b95a0a..610e63cb3 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -18,8 +18,7 @@ import os import sys import socket import ipaddress -import json -from vyos.util import cmd, rc_cmd +from vyos.util import get_all_vrfs from vyos.ifconfig import Section @@ -36,13 +35,7 @@ def vrf_list() -> list: Get list of VRFs in system :rtype: list """ - result = cmd(f'sudo ip --json --brief link show type vrf') - data = json.loads(result) - vrflist: list = [] - for o in data: - if 'ifname' in o: - vrflist.append(o['ifname']) - return vrflist + return list(get_all_vrfs().keys()) options = { diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py index 4299d6e5f..6c7030ea0 100755 --- a/src/op_mode/traceroute.py +++ b/src/op_mode/traceroute.py @@ -18,6 +18,25 @@ import os import sys import socket import ipaddress +from vyos.util import get_all_vrfs +from vyos.ifconfig import Section + + +def interface_list() -> list: + """ + Get list of interfaces in system + :rtype: list + """ + return Section.interfaces() + + +def vrf_list() -> list: + """ + Get list of VRFs in system + :rtype: list + """ + return list(get_all_vrfs().keys()) + options = { 'backward-hops': { @@ -48,6 +67,7 @@ options = { 'interface': { 'traceroute': '{command} -i {value}', 'type': '<interface>', + 'helpfunction': interface_list, 'help': 'Source interface' }, 'lookup-as': { @@ -99,6 +119,7 @@ options = { 'traceroute': 'sudo ip vrf exec {value} {command}', 'type': '<vrf>', 'help': 'Use specified VRF table', + 'helpfunction': vrf_list, 'dflt': 'default'} } @@ -108,20 +129,33 @@ traceroute = { } -class List (list): - def first (self): +class List(list): + def first(self): return self.pop(0) if self else '' def last(self): return self.pop() if self else '' - def prepend(self,value): - self.insert(0,value) + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) def expension_failure(option, completions): reason = 'Ambiguous' if completions else 'Invalid' - sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option)) + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) if completions: sys.stderr.write(' Possible completions:\n ') sys.stderr.write('\n '.join(completions)) @@ -160,30 +194,46 @@ if __name__ == '__main__': sys.exit("traceroute: Missing host") if host == '--get-options': - args.first() # pop traceroute + args.first() # pop ping args.first() # pop IP + usedoptionslist = [] while args: - option = args.first() - - matched = complete(option) + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) sys.stdout.write(' '.join(matched)) sys.exit(0) - if len(matched) > 1 : + if len(matched) > 1: sys.stdout.write(' '.join(matched)) sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) - if options[matched[0]]['type'] == 'noarg': - continue - - value = args.first() + value = args.first() # pop option's value if not args: matched = complete(option) - sys.stdout.write(options[matched[0]]['type']) + helplines = options[matched[0]]['type'] + # Run helpfunction to get list of possible values + if 'helpfunction' in options[matched[0]]: + result = options[matched[0]]['helpfunction']() + if result: + helplines = '\n' + ' '.join(result) + sys.stdout.write(helplines) sys.exit(0) - for name,option in options.items(): + for name, option in options.items(): if 'dflt' in option and name not in args: args.append(name) args.append(option['dflt']) @@ -200,8 +250,7 @@ if __name__ == '__main__': except ValueError: sys.exit(f'traceroute: Unknown host: {host}') - command = convert(traceroute[version],args) + command = convert(traceroute[version], args) # print(f'{command} {host}') os.system(f'{command} {host}') - |