diff options
Diffstat (limited to 'src')
104 files changed, 2719 insertions, 999 deletions
| diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index c65ef9540..aabf2bdf5 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -35,6 +35,7 @@ airbag.enable()  conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'  sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf' +nftables_ct_file = r'/run/nftables-ct.conf'  # Every ALG (Application Layer Gateway) consists of either a Kernel Object  # also called a Kernel Module/Driver or some rules present in iptables @@ -81,16 +82,35 @@ def get_config(config=None):      # 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) +    # XXX: T2665: we can not safely rely on the defaults() when there are +    # tagNodes in place, it is better to blend in the defaults manually. +    if 'timeout' in default_values and 'custom' in default_values['timeout']: +        del default_values['timeout']['custom']      conntrack = dict_merge(default_values, conntrack)      return conntrack  def verify(conntrack): +    if dict_search('ignore.rule', conntrack) != None: +        for rule, rule_config in conntrack['ignore']['rule'].items(): +            if dict_search('destination.port', rule_config) or \ +               dict_search('source.port', rule_config): +               if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']: +                   raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}') +      return None  def generate(conntrack):      render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack)      render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack) +    render(nftables_ct_file, 'conntrack/nftables-ct.tmpl', conntrack) + +    # dry-run newly generated configuration +    tmp = run(f'nft -c -f {nftables_ct_file}') +    if tmp > 0: +        if os.path.exists(nftables_ct_file): +            os.unlink(nftables_ct_file) +        raise ConfigError('Configuration file errors encountered!')      return None @@ -127,6 +147,9 @@ def apply(conntrack):                      if not find_nftables_ct_rule(rule):                          cmd(f'nft insert rule ip raw VYOS_CT_HELPER {rule}') +    # Load new nftables ruleset +    cmd(f'nft -f {nftables_ct_file}') +      if process_named_running('conntrackd'):          # Reload conntrack-sync daemon to fetch new sysctl values          resync_conntrackd() diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 8f9837c2b..34d1f7398 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -93,9 +93,9 @@ def verify(conntrack):              raise ConfigError('Can not configure expect-sync "all" with other protocols!')      if 'listen_address' in conntrack: -        address = conntrack['listen_address'] -        if not is_addr_assigned(address): -            raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') +        for address in conntrack['listen_address']: +            if not is_addr_assigned(address): +                raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')      vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)      if vrrp_group == None: diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 2e14e0b25..516671844 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -122,6 +122,18 @@ def verify(container):                          raise ConfigError(f'IP address "{address}" can not be used for a container, '\                                            'reserved for the container engine!') +            if 'device' in container_config: +                for dev, dev_config in container_config['device'].items(): +                    if 'source' not in dev_config: +                        raise ConfigError(f'Device "{dev}" has no source path configured!') + +                    if 'destination' not in dev_config: +                        raise ConfigError(f'Device "{dev}" has no destination path configured!') + +                    source = dev_config['source'] +                    if not os.path.exists(source): +                        raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!') +              if 'environment' in container_config:                  for var, cfg in container_config['environment'].items():                      if 'value' not in cfg: @@ -266,6 +278,14 @@ def apply(container):                      c = c.replace('-', '_')                      cap_add += f' --cap-add={c}' +            # Add a host device to the container /dev/x:/dev/x +            device = '' +            if 'device' in container_config: +                for dev, dev_config in container_config['device'].items(): +                    source_dev = dev_config['source'] +                    dest_dev = dev_config['destination'] +                    device += f' --device={source_dev}:{dest_dev}' +              # Check/set environment options "-e foo=bar"              env_opt = ''              if 'environment' in container_config: @@ -296,9 +316,9 @@ def apply(container):              container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \                                   f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ -                                 f'--name {name} {port} {volume} {env_opt}' +                                 f'--name {name} {device} {port} {volume} {env_opt}'              if 'allow_host_networks' in container_config: -                _cmd(f'{container_base_cmd} --net host {image}') +                run(f'{container_base_cmd} --net host {image}')              else:                  for network in container_config['network']:                      ipparam = '' @@ -306,19 +326,25 @@ def apply(container):                          address = container_config['network'][network]['address']                          ipparam = f'--ip {address}' -                    counter = 0 -                    while True: -                        if counter >= 10: -                            break -                        try: -                            _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}') -                            break -                        except: -                            counter = counter +1 -                            sleep(0.5) +                    run(f'{container_base_cmd} --net {network} {ipparam} {image}')      return None +def run(container_cmd): +    counter = 0 +    while True: +        if counter >= 10: +            break +        try: +            _cmd(container_cmd) +            break +        except: +            counter = counter +1 +            sleep(0.5) + +    return None + +  if __name__ == '__main__':      try:          c = get_config() diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 23a16df63..fa9b21f20 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -16,6 +16,7 @@  import os +from netifaces import interfaces  from sys import exit  from glob import glob @@ -65,10 +66,6 @@ def get_config(config=None):          if conf.exists(base_nameservers):              dns.update({'system_name_server': conf.return_values(base_nameservers)}) -        base_nameservers_dhcp = ['system', 'name-servers-dhcp'] -        if conf.exists(base_nameservers_dhcp): -            dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)}) -      if 'authoritative_domain' in dns:          dns['authoritative_zones'] = []          dns['authoritative_zone_errors'] = [] @@ -272,9 +269,8 @@ def verify(dns):          raise ConfigError('Invalid authoritative records have been defined')      if 'system' in dns: -        if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns): -            print("Warning: No 'system name-server' or 'system " \ -                  "name-servers-dhcp' configured") +        if not 'system_name_server' in dns: +            print('Warning: No "system name-server" configured')      return None @@ -339,10 +335,15 @@ def apply(dns):              hc.delete_name_server_tags_recursor(['system'])          # add dhcp nameserver tags for configured interfaces -        if 'system_name_server_dhcp' in dns: -            for interface in dns['system_name_server_dhcp']: -                hc.add_name_server_tags_recursor(['dhcp-' + interface, -                                                  'dhcpv6-' + interface ]) +        if 'system_name_server' in dns: +            for interface in dns['system_name_server']: +                # system_name_server key contains both IP addresses and interface +                # names (DHCP) to use DNS servers. We need to check if the +                # value is an interface name - only if this is the case, add the +                # interface based DNS forwarder. +                if interface in interfaces(): +                    hc.add_name_server_tags_recursor(['dhcp-' + interface, +                                                      'dhcpv6-' + interface ])          # hostsd will generate the forward-zones file          # the list and keys() are required as get returns a dict, not list diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py index b0df9dff4..9a5d278e9 100755 --- a/src/conf_mode/firewall-interface.py +++ b/src/conf_mode/firewall-interface.py @@ -31,6 +31,9 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() +NAME_PREFIX = 'NAME_' +NAME6_PREFIX = 'NAME6_' +  NFT_CHAINS = {      'in': 'VYOS_FW_FORWARD',      'out': 'VYOS_FW_FORWARD', @@ -127,7 +130,7 @@ def apply(if_firewall):          name = dict_search_args(if_firewall, direction, 'name')          if name: -            rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, name) +            rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}')              if not rule_exists:                  rule_action = 'insert' @@ -138,24 +141,24 @@ def apply(if_firewall):                      rule_action = 'add'                      rule_prefix = f'position {handle}' -                run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {name}') +                run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}')          else:              cleanup_rule('ip filter', chain, if_prefix, ifname)          ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')          if ipv6_name: -            rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, ipv6_name) +            rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}')              if not rule_exists:                  rule_action = 'insert'                  rule_prefix = '' -                handle = state_policy_handle('ip filter', chain) +                handle = state_policy_handle('ip6 filter', ipv6_chain)                  if handle:                      rule_action = 'add'                      rule_prefix = f'position {handle}' -                run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {ipv6_name}') +                run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}')          else:              cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 75382034f..f33198a49 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os +import re  from glob import glob  from json import loads @@ -22,6 +23,7 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.configdict import node_changed  from vyos.configdiff import get_config_diff, Diff  from vyos.template import render  from vyos.util import cmd @@ -33,7 +35,10 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() +policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' +  nftables_conf = '/run/nftables.conf' +nftables_defines_conf = '/run/nftables_defines.conf'  sysfs_config = {      'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, @@ -49,6 +54,9 @@ sysfs_config = {      'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}  } +NAME_PREFIX = 'NAME_' +NAME6_PREFIX = 'NAME6_' +  preserve_chains = [      'INPUT',      'FORWARD', @@ -65,6 +73,9 @@ preserve_chains = [      'VYOS_FRAG6_MARK'  ] +nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'] +nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] +  valid_groups = [      'address_group',      'network_group', @@ -97,6 +108,35 @@ def get_firewall_interfaces(conf):          out.update(find_interfaces(iftype_conf))      return out +def get_firewall_zones(conf): +    used_v4 = [] +    used_v6 = [] +    zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True, +                                    no_tag_node_value_mangle=True) + +    if 'zone' in zone_policy: +        for zone, zone_conf in zone_policy['zone'].items(): +            if 'from' in zone_conf: +                for from_zone, from_conf in zone_conf['from'].items(): +                    name = dict_search_args(from_conf, 'firewall', 'name') +                    if name: +                        used_v4.append(name) + +                    ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') +                    if ipv6_name: +                        used_v6.append(ipv6_name) + +            if 'intra_zone_filtering' in zone_conf: +                name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name') +                if name: +                    used_v4.append(name) + +                ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') +                if ipv6_name: +                    used_v6.append(ipv6_name) + +    return {'name': used_v4, 'ipv6_name': used_v6} +  def get_config(config=None):      if config:          conf = config @@ -104,16 +144,15 @@ def get_config(config=None):          conf = Config()      base = ['firewall'] -    if not conf.exists(base): -        return {} -      firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,                                      no_tag_node_value_mangle=True)      default_values = defaults(base)      firewall = dict_merge(default_values, firewall) +    firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))      firewall['interfaces'] = get_firewall_interfaces(conf) +    firewall['zone_policy'] = get_firewall_zones(conf)      if 'config_trap' in firewall and firewall['config_trap'] == 'enable':          diff = get_config_diff(conf) @@ -121,6 +160,7 @@ def get_config(config=None):          firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],                                          key_mangling=('-', '_'), get_first_key=True,                                          no_tag_node_value_mangle=True) +      return firewall  def verify_rule(firewall, rule_conf, ipv6): @@ -131,6 +171,12 @@ def verify_rule(firewall, rule_conf, ipv6):          if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):              raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') +    if 'limit' in rule_conf: +        if 'rate' in rule_conf['limit']: +            rate_int = re.sub(r'\D', '', rule_conf['limit']['rate']) +            if int(rate_int) < 1: +                raise ConfigError('Limit rate integer cannot be less than 1') +      if 'ipsec' in rule_conf:          if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']):              raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') @@ -139,6 +185,23 @@ def verify_rule(firewall, rule_conf, ipv6):          if not {'count', 'time'} <= set(rule_conf['recent']):              raise ConfigError('Recent "count" and "time" values must be defined') +    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') +    if tcp_flags: +        if dict_search_args(rule_conf, 'protocol') != 'tcp': +            raise ConfigError('Protocol must be tcp when specifying tcp flags') + +        not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') +        if not_flags: +            duplicates = [flag for flag in tcp_flags if flag in not_flags] +            if duplicates: +                raise ConfigError(f'Cannot match a tcp flag as set and not set') + +    if 'protocol' in rule_conf: +        if rule_conf['protocol'] == 'icmp' and ipv6: +            raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp') +        if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6: +            raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp') +      for side in ['destination', 'source']:          if side in rule_conf:              side_conf = rule_conf[side] @@ -151,16 +214,19 @@ def verify_rule(firewall, rule_conf, ipv6):                      if group in side_conf['group']:                          group_name = side_conf['group'][group] -                        fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group +                        if group_name and group_name[0] == '!': +                            group_name = group_name[1:] -                        if not dict_search_args(firewall, 'group', fw_group): -                            error_group = fw_group.replace("_", "-") -                            raise ConfigError(f'Group defined in rule but {error_group} is not configured') +                        fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group +                        error_group = fw_group.replace("_", "-") +                        group_obj = dict_search_args(firewall, 'group', fw_group, group_name) -                        if group_name not in firewall['group'][fw_group]: -                            error_group = group.replace("_", "-") +                        if group_obj is None:                              raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') +                        if not group_obj: +                            print(f'WARNING: {error_group} "{group_name}" has no members') +              if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):                  if 'protocol' not in rule_conf:                      raise ConfigError('Protocol must be defined if specifying a port or port-group') @@ -169,10 +235,6 @@ def verify_rule(firewall, rule_conf, ipv6):                      raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')  def verify(firewall): -    # bail out early - looks like removal from running config -    if not firewall: -        return None -      if 'config_trap' in firewall and firewall['config_trap'] == 'enable':          if not firewall['trap_targets']:              raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') @@ -195,16 +257,34 @@ def verify(firewall):              name = dict_search_args(if_firewall, direction, 'name')              ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') -            if name and not dict_search_args(firewall, 'name', name): +            if name and dict_search_args(firewall, 'name', name) == None:                  raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}') -            if ipv6_name and not dict_search_args(firewall, 'ipv6_name', ipv6_name): +            if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:                  raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}') +    for fw_name, used_names in firewall['zone_policy'].items(): +        for name in used_names: +            if dict_search_args(firewall, fw_name, name) == None: +                raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') +      return None +def cleanup_rule(table, jump_chain): +    commands = [] +    chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains +    for chain in chains: +        results = cmd(f'nft -a list chain {table} {chain}').split("\n") +        for line in results: +            if f'jump {jump_chain}' in line: +                handle_search = re.search('handle (\d+)', line) +                if handle_search: +                    commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') +    return commands +  def cleanup_commands(firewall):      commands = [] +    commands_end = []      for table in ['ip filter', 'ip6 filter']:          state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6'          json_str = cmd(f'nft -j list table {table}') @@ -220,11 +300,12 @@ def cleanup_commands(firewall):                      else:                          commands.append(f'flush chain {table} {chain}')                  elif chain not in preserve_chains and not chain.startswith("VZONE"): -                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain): +                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None:                          commands.append(f'flush chain {table} {chain}') -                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain): +                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None:                          commands.append(f'flush chain {table} {chain}')                      else: +                        commands += cleanup_rule(table, chain)                          commands.append(f'delete chain {table} {chain}')              elif 'rule' in item:                  rule = item['rule'] @@ -234,7 +315,10 @@ def cleanup_commands(firewall):                              chain = rule['chain']                              handle = rule['handle']                              commands.append(f'delete rule {table} {chain} handle {handle}') -    return commands +            elif 'set' in item: +                set_name = item['set']['name'] +                commands_end.append(f'delete set {table} {set_name}') +    return commands + commands_end  def generate(firewall):      if not os.path.exists(nftables_conf): @@ -243,6 +327,7 @@ def generate(firewall):          firewall['cleanup_commands'] = cleanup_commands(firewall)      render(nftables_conf, 'firewall/nftables.tmpl', firewall) +    render(nftables_defines_conf, 'firewall/nftables-defines.tmpl', firewall)      return None  def apply_sysfs(firewall): @@ -306,6 +391,12 @@ def state_policy_rule_exists():      search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD')      return 'VYOS_STATE_POLICY' in search_str +def resync_policy_route(): +    # Update policy route as firewall groups were updated +    tmp = run(policy_route_conf_script) +    if tmp > 0: +        print('Warning: Failed to re-apply policy route configuration') +  def apply(firewall):      if 'first_install' in firewall:          run('nfct helper add rpc inet tcp') @@ -325,6 +416,9 @@ def apply(firewall):      apply_sysfs(firewall) +    if firewall['policy_resync']: +        resync_policy_route() +      post_apply_trap(firewall)      return None diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 975f19acf..25bf54790 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -27,6 +27,7 @@ from vyos.configdict import dict_merge  from vyos.ifconfig import Section  from vyos.ifconfig import Interface  from vyos.template import render +from vyos.util import call  from vyos.util import cmd  from vyos.validate import is_addr_assigned  from vyos.xml import defaults @@ -35,6 +36,8 @@ from vyos import airbag  airbag.enable()  uacctd_conf_path = '/run/pmacct/uacctd.conf' +systemd_service = 'uacctd.service' +systemd_override = f'/etc/systemd/system/{systemd_service}.d/override.conf'  nftables_nflog_table = 'raw'  nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'  egress_nftables_nflog_table = 'inet mangle' @@ -236,7 +239,10 @@ def generate(flow_config):      if not flow_config:          return None -    render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', flow_config) +    render(uacctd_conf_path, 'pmacct/uacctd.conf.tmpl', flow_config) +    render(systemd_override, 'pmacct/override.conf.tmpl', flow_config) +    # Reload systemd manager configuration +    call('systemctl daemon-reload')  def apply(flow_config):      action = 'restart' @@ -246,13 +252,13 @@ def apply(flow_config):          _nftables_config([], 'egress')          # Stop flow-accounting daemon and remove configuration file -        cmd('systemctl stop uacctd.service') +        call(f'systemctl stop {systemd_service}')          if os.path.exists(uacctd_conf_path):              os.unlink(uacctd_conf_path)          return      # Start/reload flow-accounting daemon -    cmd(f'systemctl restart uacctd.service') +    call(f'systemctl restart {systemd_service}')      # configure nftables rules for defined interfaces      if 'interface' in flow_config: diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index b5f5e919f..00f3d4f7f 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -66,6 +66,15 @@ def get_config(config=None):      if conf.exists('debug'):          http_api['debug'] = True +    # this node is not available by CLI by default, and is reserved for +    # the graphql tools. One can enable it for testing, with the warning +    # that this will open an unauthenticated server. To do so +    # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql +    # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def +    # and configure; editing the config alone is insufficient. +    if conf.exists('gql'): +        http_api['gql'] = True +      if conf.exists('socket'):          http_api['socket'] = True diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 431d65f1f..ad5a0f499 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -27,8 +27,9 @@ from vyos.configdict import is_source_interface  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_dhcpv6 -from vyos.configverify import verify_source_interface +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_source_interface  from vyos.configverify import verify_vlan_config  from vyos.configverify import verify_vrf  from vyos.ifconfig import BondIf @@ -132,10 +133,10 @@ def verify(bond):          return None      if 'arp_monitor' in bond: -        if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: +        if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:              raise ConfigError('The maximum number of arp-monitor targets is 16') -        if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: +        if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:              if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:                  raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \                                    'transmit-load-balance or adaptive-load-balance') @@ -149,6 +150,7 @@ def verify(bond):      verify_address(bond)      verify_dhcpv6(bond)      verify_vrf(bond) +    verify_mirror_redirect(bond)      # use common function to verify VLAN configuration      verify_vlan_config(bond) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 4d3ebc587..b1f7e6d7c 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -22,12 +22,12 @@ from netifaces import interfaces  from vyos.config import Config  from vyos.configdict import get_interface_dict  from vyos.configdict import node_changed -from vyos.configdict import leaf_node_changed  from vyos.configdict import is_member  from vyos.configdict import is_source_interface  from vyos.configdict import has_vlan_subinterface_configured  from vyos.configdict import dict_merge  from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_vrf  from vyos.ifconfig import BridgeIf  from vyos.validate import has_address_configured @@ -106,6 +106,7 @@ def verify(bridge):      verify_dhcpv6(bridge)      verify_vrf(bridge) +    verify_mirror_redirect(bridge)      ifname = bridge['ifname'] diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 55c783f38..4a1eb7b93 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -21,6 +21,7 @@ from vyos.configdict import get_interface_dict  from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import DummyIf  from vyos import ConfigError  from vyos import airbag @@ -46,6 +47,7 @@ def verify(dummy):      verify_vrf(dummy)      verify_address(dummy) +    verify_mirror_redirect(dummy)      return None diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e7250fb49..6aea7a80e 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -25,14 +25,16 @@ from vyos.configverify import verify_address  from vyos.configverify import verify_dhcpv6  from vyos.configverify import verify_eapol  from vyos.configverify import verify_interface_exists -from vyos.configverify import verify_mirror +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_mtu  from vyos.configverify import verify_mtu_ipv6  from vyos.configverify import verify_vlan_config  from vyos.configverify import verify_vrf  from vyos.ethtool import Ethtool  from vyos.ifconfig import EthernetIf -from vyos.pki import wrap_certificate +from vyos.pki import find_chain +from vyos.pki import encode_certificate +from vyos.pki import load_certificate  from vyos.pki import wrap_private_key  from vyos.template import render  from vyos.util import call @@ -81,7 +83,7 @@ def verify(ethernet):      verify_address(ethernet)      verify_vrf(ethernet)      verify_eapol(ethernet) -    verify_mirror(ethernet) +    verify_mirror_redirect(ethernet)      ethtool = Ethtool(ifname)      # No need to check speed and duplex keys as both have default values. @@ -159,16 +161,26 @@ def generate(ethernet):          cert_name = ethernet['eapol']['certificate']          pki_cert = ethernet['pki']['certificate'][cert_name] -        write_file(cert_file_path, wrap_certificate(pki_cert['certificate'])) +        loaded_pki_cert = load_certificate(pki_cert['certificate']) +        loaded_ca_certs = {load_certificate(c['certificate']) +            for c in ethernet['pki']['ca'].values()} + +        cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) + +        write_file(cert_file_path, +                   '\n'.join(encode_certificate(c) for c in cert_full_chain))          write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))          if 'ca_certificate' in ethernet['eapol']:              ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')              ca_cert_name = ethernet['eapol']['ca_certificate'] -            pki_ca_cert = ethernet['pki']['ca'][cert_name] +            pki_ca_cert = ethernet['pki']['ca'][ca_cert_name] + +            loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) +            ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)              write_file(ca_cert_file_path, -                       wrap_certificate(pki_ca_cert['certificate'])) +                       '\n'.join(encode_certificate(c) for c in ca_full_chain))      else:          # delete configuration on interface removal          if os.path.isfile(wpa_suppl_conf.format(**ethernet)): diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 2a63b60aa..3a668226b 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -24,6 +24,7 @@ from vyos.configdict import get_interface_dict  from vyos.configverify import verify_address  from vyos.configverify import verify_mtu_ipv6  from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import GeneveIf  from vyos import ConfigError @@ -50,6 +51,7 @@ def verify(geneve):      verify_mtu_ipv6(geneve)      verify_address(geneve) +    verify_mirror_redirect(geneve)      if 'remote' not in geneve:          raise ConfigError('Remote side must be configured') diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 9b6ddd5aa..22256bf4f 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import L2TPv3If  from vyos.util import check_kmod  from vyos.validate import is_addr_assigned @@ -76,6 +77,7 @@ def verify(l2tpv3):      verify_mtu_ipv6(l2tpv3)      verify_address(l2tpv3) +    verify_mirror_redirect(l2tpv3)      return None  def generate(l2tpv3): diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 193334443..e4bc15bb5 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -20,6 +20,7 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_interface_dict +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import LoopbackIf  from vyos import ConfigError  from vyos import airbag @@ -39,6 +40,7 @@ def get_config(config=None):      return loopback  def verify(loopback): +    verify_mirror_redirect(loopback)      return None  def generate(loopback): diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index eab69f36e..96fc1c41c 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -29,6 +29,7 @@ from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_source_interface  from vyos import ConfigError  from vyos import airbag @@ -66,6 +67,7 @@ def verify(macsec):      verify_vrf(macsec)      verify_mtu_ipv6(macsec)      verify_address(macsec) +    verify_mirror_redirect(macsec)      if not (('security' in macsec) and              ('cipher' in macsec['security'])): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 3b8fae710..83d1c6d9b 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.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 @@ -32,8 +32,10 @@ from shutil import rmtree  from vyos.config import Config  from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed  from vyos.configverify import verify_vrf  from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import VTunIf  from vyos.pki import load_dh_parameters  from vyos.pki import load_private_key @@ -47,6 +49,7 @@ from vyos.template import is_ipv4  from vyos.template import is_ipv6  from vyos.util import call  from vyos.util import chown +from vyos.util import cmd  from vyos.util import dict_search  from vyos.util import dict_search_args  from vyos.util import makedir @@ -87,6 +90,9 @@ def get_config(config=None):      if 'deleted' not in openvpn:          openvpn['pki'] = tmp_pki +        tmp = leaf_node_changed(conf, ['openvpn-option']) +        if tmp: openvpn['restart_required'] = '' +          # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'          # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.          tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) @@ -225,11 +231,12 @@ def verify(openvpn):          if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:              raise ConfigError('Must specify "local-address" or add interface to bridge') -        if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: -            raise ConfigError('Only one IPv4 local-address can be specified') +        if 'local_address' in openvpn: +            if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: +                raise ConfigError('Only one IPv4 local-address can be specified') -        if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: -            raise ConfigError('Only one IPv6 local-address can be specified') +            if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: +                raise ConfigError('Only one IPv6 local-address can be specified')          if openvpn['device_type'] == 'tun':              if 'remote_address' not in openvpn: @@ -268,7 +275,7 @@ def verify(openvpn):              if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):                  raise ConfigError('"remote-address" and "remote-host" can not be the same') -        if openvpn['device_type'] == 'tap': +        if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:              # we can only have one local_address, this is ensured above              v4addr = None              for laddr in openvpn['local_address']: @@ -423,8 +430,8 @@ def verify(openvpn):      # verify specified IP address is present on any interface on this system      if 'local_host' in openvpn:          if not is_addr_assigned(openvpn['local_host']): -            raise ConfigError('local-host IP address "{local_host}" not assigned' \ -                              ' to any interface'.format(**openvpn)) +            print('local-host IP address "{local_host}" not assigned' \ +                  ' to any interface'.format(**openvpn))      # TCP active      if openvpn['protocol'] == 'tcp-active': @@ -489,6 +496,7 @@ def verify(openvpn):              raise ConfigError('Username for authentication is missing')      verify_vrf(openvpn) +    verify_mirror_redirect(openvpn)      return None @@ -647,9 +655,19 @@ def apply(openvpn):          return None +    # verify specified IP address is present on any interface on this system +    # Allow to bind service to nonlocal address, if it virtaual-vrrp address +    # or if address will be assign later +    if 'local_host' in openvpn: +        if not is_addr_assigned(openvpn['local_host']): +            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1') +      # No matching OpenVPN process running - maybe it got killed or none      # existed - nevertheless, spawn new OpenVPN process -    call(f'systemctl reload-or-restart openvpn@{interface}.service') +    action = 'reload-or-restart' +    if 'restart_required' in openvpn: +        action = 'restart' +    call(f'systemctl {action} openvpn@{interface}.service')      o = VTunIf(**openvpn)      o.update(openvpn) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 584adc75e..bfb1fadd5 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -28,6 +28,7 @@ from vyos.configverify import verify_source_interface  from vyos.configverify import verify_interface_exists  from vyos.configverify import verify_vrf  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import PPPoEIf  from vyos.template import render  from vyos.util import call @@ -85,6 +86,7 @@ def verify(pppoe):      verify_authentication(pppoe)      verify_vrf(pppoe)      verify_mtu_ipv6(pppoe) +    verify_mirror_redirect(pppoe)      if {'connect_on_demand', 'vrf'} <= set(pppoe):          raise ConfigError('On-demand dialing and VRF can not be used at the same time') diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 945a2ea9c..f2c85554f 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -25,6 +25,7 @@ from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_source_interface  from vyos.configverify import verify_vlan_config  from vyos.configverify import verify_mtu_parent +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import MACVLANIf  from vyos import ConfigError @@ -60,6 +61,7 @@ def verify(peth):      verify_vrf(peth)      verify_address(peth)      verify_mtu_parent(peth, peth['parent']) +    verify_mirror_redirect(peth)      # use common function to verify VLAN configuration      verify_vlan_config(peth) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 30f57ec0c..f4668d976 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 yOS 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 @@ -18,24 +18,20 @@ import os  from sys import exit  from netifaces import interfaces -from ipaddress import IPv4Address  from vyos.config import Config -from vyos.configdict import dict_merge  from vyos.configdict import get_interface_dict -from vyos.configdict import node_changed  from vyos.configdict import leaf_node_changed  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_interface_exists  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_vrf  from vyos.configverify import verify_tunnel  from vyos.ifconfig import Interface  from vyos.ifconfig import Section  from vyos.ifconfig import TunnelIf -from vyos.template import is_ipv4 -from vyos.template import is_ipv6  from vyos.util import get_interface_config  from vyos.util import dict_search  from vyos import ConfigError @@ -54,8 +50,24 @@ def get_config(config=None):      base = ['interfaces', 'tunnel']      tunnel = get_interface_dict(conf, base) -    tmp = leaf_node_changed(conf, ['encapsulation']) -    if tmp: tunnel.update({'encapsulation_changed': {}}) +    if 'deleted' not in tunnel: +        tmp = leaf_node_changed(conf, ['encapsulation']) +        if tmp: tunnel.update({'encapsulation_changed': {}}) + +        # We also need to inspect other configured tunnels as there are Kernel +        # restrictions where we need to comply. E.g. GRE tunnel key can't be used +        # twice, or with multiple GRE tunnels to the same location we must specify +        # a GRE key +        conf.set_level(base) +        tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'), +                                                      get_first_key=True, +                                                      no_tag_node_value_mangle=True) +        # delete our own instance from this dict +        ifname = tunnel['ifname'] +        del tunnel['other_tunnels'][ifname] +        # if only one tunnel is present on the system, no need to keep this key +        if len(tunnel['other_tunnels']) == 0: +            del tunnel['other_tunnels']      # We must check if our interface is configured to be a DMVPN member      nhrp_base = ['protocols', 'nhrp', 'tunnel'] @@ -96,35 +108,47 @@ def verify(tunnel):              if 'direction' not in tunnel['parameters']['erspan']:                  raise ConfigError('ERSPAN version 2 requires direction to be set!') -    # If tunnel source address any and key not set +    # If tunnel source is any and gre key is not set +    interface = tunnel['ifname']      if tunnel['encapsulation'] in ['gre'] and \         dict_search('source_address', tunnel) == '0.0.0.0' and \         dict_search('parameters.ip.key', tunnel) == None: -        raise ConfigError('Tunnel parameters ip key must be set!') - -    if tunnel['encapsulation'] in ['gre', 'gretap']: -        if dict_search('parameters.ip.key', tunnel) != None: -            # Check pairs tunnel source-address/encapsulation/key with exists tunnels. -            # Prevent the same key for 2 tunnels with same source-address/encap. T2920 -            for tunnel_if in Section.interfaces('tunnel'): -                # It makes no sense to run the test for re-used GRE keys on our -                # own interface we are currently working on -                if tunnel['ifname'] == tunnel_if: -                    continue -                tunnel_cfg = get_interface_config(tunnel_if) -                # no match on encapsulation - bail out -                if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: -                    continue -                new_source_address = dict_search('source_address', tunnel) -                # Convert tunnel key to ip key, format "ip -j link show" -                # 1 => 0.0.0.1, 999 => 0.0.3.231 -                orig_new_key = dict_search('parameters.ip.key', tunnel) -                new_key = IPv4Address(int(orig_new_key)) -                new_key = str(new_key) -                if dict_search('address', tunnel_cfg) == new_source_address and \ -                   dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: -                    raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ +        raise ConfigError(f'"parameters ip key" must be set for {interface} when '\ +                           'encapsulation is GRE!') + +    gre_encapsulations = ['gre', 'gretap'] +    if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel: +        # Check pairs tunnel source-address/encapsulation/key with exists tunnels. +        # Prevent the same key for 2 tunnels with same source-address/encap. T2920 +        for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items(): +            # no match on encapsulation - bail out +            our_encapsulation = tunnel['encapsulation'] +            their_encapsulation = o_tunnel_conf['encapsulation'] +            if our_encapsulation in gre_encapsulations and their_encapsulation \ +                not in gre_encapsulations: +                continue + +            our_address = dict_search('source_address', tunnel) +            our_key = dict_search('parameters.ip.key', tunnel) +            their_address = dict_search('source_address', o_tunnel_conf) +            their_key = dict_search('parameters.ip.key', o_tunnel_conf) +            if our_key != None: +                if their_address == our_address and their_key == our_key: +                    raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \                                        f'is already used for tunnel "{tunnel_if}"!') +            else: +                our_source_if = dict_search('source_interface', tunnel) +                their_source_if = dict_search('source_interface', o_tunnel_conf) +                our_remote = dict_search('remote', tunnel) +                their_remote = dict_search('remote', o_tunnel_conf) +                # If no IP GRE key is defined we can not have more then one GRE tunnel +                # bound to any one interface/IP address and the same remote. This will +                # result in a OS  PermissionError: add tunnel "gre0" failed: File exists +                if (their_address == our_address or our_source_if == their_source_if) and \ +                    our_remote == their_remote: +                    raise ConfigError(f'Missing required "ip key" parameter when '\ +                                       'running more then one GRE based tunnel on the '\ +                                       'same source-interface/source-address')      # Keys are not allowed with ipip and sit tunnels      if tunnel['encapsulation'] in ['ipip', 'sit']: @@ -134,6 +158,7 @@ def verify(tunnel):      verify_mtu_ipv6(tunnel)      verify_address(tunnel)      verify_vrf(tunnel) +    verify_mirror_redirect(tunnel)      if 'source_interface' in tunnel:          verify_interface_exists(tunnel['source_interface']) diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py index 57950ffea..f06fdff1b 100755 --- a/src/conf_mode/interfaces-vti.py +++ b/src/conf_mode/interfaces-vti.py @@ -19,6 +19,7 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_interface_dict +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import VTIIf  from vyos.util import dict_search  from vyos import ConfigError @@ -39,6 +40,7 @@ def get_config(config=None):      return vti  def verify(vti): +    verify_mirror_redirect(vti)      return None  def generate(vti): diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 1f097c4e3..0a9b51cac 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.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 @@ -21,9 +21,11 @@ from netifaces import interfaces  from vyos.config import Config  from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_source_interface  from vyos.ifconfig import Interface  from vyos.ifconfig import VXLANIf @@ -34,8 +36,8 @@ airbag.enable()  def get_config(config=None):      """ -    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the -    interface name will be added or a deleted flag +    Retrive CLI config as dictionary. Dictionary can never be empty, as at least +    the interface name will be added or a deleted flag      """      if config:          conf = config @@ -44,6 +46,16 @@ def get_config(config=None):      base = ['interfaces', 'vxlan']      vxlan = get_interface_dict(conf, base) +    # VXLAN interfaces are picky and require recreation if certain parameters +    # change. But a VXLAN interface should - of course - not be re-created if +    # it's description or IP address is adjusted. Feels somehow logic doesn't it? +    for cli_option in ['external', 'gpe', 'group', 'port', 'remote', +                       'source-address', 'source-interface', 'vni', +                       'parameters ip dont-fragment', 'parameters ip tos', +                       'parameters ip ttl']: +        if leaf_node_changed(conf, cli_option.split()): +            vxlan.update({'rebuild_required': {}}) +      # We need to verify that no other VXLAN tunnel is configured when external      # mode is in use - Linux Kernel limitation      conf.set_level(base) @@ -70,8 +82,7 @@ def verify(vxlan):      if 'group' in vxlan:          if 'source_interface' not in vxlan: -            raise ConfigError('Multicast VXLAN requires an underlaying interface ') - +            raise ConfigError('Multicast VXLAN requires an underlaying interface')          verify_source_interface(vxlan)      if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): @@ -108,22 +119,42 @@ def verify(vxlan):              raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\                                f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)') +    # Check for mixed IPv4 and IPv6 addresses +    protocol = None +    if 'source_address' in vxlan: +        if is_ipv6(vxlan['source_address']): +            protocol = 'ipv6' +        else: +            protocol = 'ipv4' + +    if 'remote' in vxlan: +        error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay' +        for remote in vxlan['remote']: +            if is_ipv6(remote): +                if protocol == 'ipv4': +                    raise ConfigError(error_msg) +                protocol = 'ipv6' +            else: +                if protocol == 'ipv6': +                    raise ConfigError(error_msg) +                protocol = 'ipv4' +      verify_mtu_ipv6(vxlan)      verify_address(vxlan) +    verify_mirror_redirect(vxlan)      return None -  def generate(vxlan):      return None -  def apply(vxlan):      # Check if the VXLAN interface already exists -    if vxlan['ifname'] in interfaces(): -        v = VXLANIf(vxlan['ifname']) -        # VXLAN is super picky and the tunnel always needs to be recreated, -        # thus we can simply always delete it first. -        v.remove() +    if 'rebuild_required' in vxlan or 'delete' in vxlan: +        if vxlan['ifname'] in interfaces(): +            v = VXLANIf(vxlan['ifname']) +            # VXLAN is super picky and the tunnel always needs to be recreated, +            # thus we can simply always delete it first. +            v.remove()      if 'deleted' not in vxlan:          # Finally create the new interface @@ -132,7 +163,6 @@ def apply(vxlan):      return None -  if __name__ == '__main__':      try:          c = get_config() diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index da64dd076..b404375d6 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_mtu_ipv6 +from vyos.configverify import verify_mirror_redirect  from vyos.ifconfig import WireGuardIf  from vyos.util import check_kmod  from vyos.util import check_port_availability @@ -70,6 +71,7 @@ def verify(wireguard):      verify_mtu_ipv6(wireguard)      verify_address(wireguard)      verify_vrf(wireguard) +    verify_mirror_redirect(wireguard)      if 'private_key' not in wireguard:          raise ConfigError('Wireguard private-key not defined') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index af35b5f03..500952df1 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -27,6 +27,7 @@ from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_dhcpv6  from vyos.configverify import verify_source_interface +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_vlan_config  from vyos.configverify import verify_vrf  from vyos.ifconfig import WiFiIf @@ -189,6 +190,7 @@ def verify(wifi):      verify_address(wifi)      verify_vrf(wifi) +    verify_mirror_redirect(wifi)      # use common function to verify VLAN configuration      verify_vlan_config(wifi) diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index a4b033374..9a33039a3 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -21,8 +21,10 @@ from time import sleep  from vyos.config import Config  from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed  from vyos.configverify import verify_authentication  from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_mirror_redirect  from vyos.configverify import verify_vrf  from vyos.ifconfig import WWANIf  from vyos.util import cmd @@ -36,7 +38,7 @@ from vyos import airbag  airbag.enable()  service_name = 'ModemManager.service' -cron_script = '/etc/cron.d/wwan' +cron_script = '/etc/cron.d/vyos-wwan'  def get_config(config=None):      """ @@ -50,6 +52,32 @@ def get_config(config=None):      base = ['interfaces', 'wwan']      wwan = get_interface_dict(conf, base) +    # We should only terminate the WWAN session if critical parameters change. +    # All parameters that can be changed on-the-fly (like interface description) +    # should not lead to a reconnect! +    tmp = leaf_node_changed(conf, ['address']) +    if tmp: wwan.update({'shutdown_required': {}}) + +    tmp = leaf_node_changed(conf, ['apn']) +    if tmp: wwan.update({'shutdown_required': {}}) + +    tmp = leaf_node_changed(conf, ['disable']) +    if tmp: wwan.update({'shutdown_required': {}}) + +    tmp = leaf_node_changed(conf, ['vrf']) +    # leaf_node_changed() returns a list, as VRF is a non-multi node, there +    # will be only one list element +    if tmp: wwan.update({'vrf_old': tmp[0]}) + +    tmp = leaf_node_changed(conf, ['authentication', 'user']) +    if tmp: wwan.update({'shutdown_required': {}}) + +    tmp = leaf_node_changed(conf, ['authentication', 'password']) +    if tmp: wwan.update({'shutdown_required': {}}) + +    tmp = leaf_node_changed(conf, ['ipv6', 'address', 'autoconf']) +    if tmp: wwan.update({'shutdown_required': {}}) +      # We need to know the amount of other WWAN interfaces as ModemManager needs      # to be started or stopped.      conf.set_level(base) @@ -57,8 +85,8 @@ def get_config(config=None):                                                      get_first_key=True,                                                      no_tag_node_value_mangle=True) -    # This if-clause is just to be sure - it will always evaluate to true      ifname = wwan['ifname'] +    # This if-clause is just to be sure - it will always evaluate to true      if ifname in wwan['other_interfaces']:          del wwan['other_interfaces'][ifname]      if len(wwan['other_interfaces']) == 0: @@ -77,18 +105,31 @@ def verify(wwan):      verify_interface_exists(ifname)      verify_authentication(wwan)      verify_vrf(wwan) +    verify_mirror_redirect(wwan)      return None  def generate(wwan):      if 'deleted' in wwan: +        # We are the last WWAN interface - there are no other ones remaining +        # thus the cronjob needs to go away, too +        if 'other_interfaces' not in wwan: +            if os.path.exists(cron_script): +                os.unlink(cron_script)          return None +    # Install cron triggered helper script to re-dial WWAN interfaces on +    # disconnect - e.g. happens during RF signal loss. The script watches every +    # WWAN interface - so there is only one instance.      if not os.path.exists(cron_script):          write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py') +      return None  def apply(wwan): +    # ModemManager is required to dial WWAN connections - one instance is +    # required to serve all modems. Activate ModemManager on first invocation +    # of any WWAN interface.      if not is_systemd_service_active(service_name):          cmd(f'systemctl start {service_name}') @@ -101,17 +142,19 @@ def apply(wwan):                  break              sleep(0.250) -    # we only need the modem number. wwan0 -> 0, wwan1 -> 1 -    modem = wwan['ifname'].lstrip('wwan') -    base_cmd = f'mmcli --modem {modem}' -    # Number of bearers is limited - always disconnect first -    cmd(f'{base_cmd} --simple-disconnect') +    if 'shutdown_required' in wwan: +        # we only need the modem number. wwan0 -> 0, wwan1 -> 1 +        modem = wwan['ifname'].lstrip('wwan') +        base_cmd = f'mmcli --modem {modem}' +        # Number of bearers is limited - always disconnect first +        cmd(f'{base_cmd} --simple-disconnect')      w = WWANIf(wwan['ifname'])      if 'deleted' in wwan or 'disable' in wwan:          w.remove() -        # There are no other WWAN interfaces - stop the daemon +        # We are the last WWAN interface - there are no other WWAN interfaces +        # remaining, thus we can stop ModemManager and free resources.          if 'other_interfaces' not in wwan:              cmd(f'systemctl stop {service_name}')              # Clean CRON helper script which is used for to re-connect when @@ -121,27 +164,25 @@ def apply(wwan):          return None -    ip_type = 'ipv4' -    slaac = dict_search('ipv6.address.autoconf', wwan) != None -    if 'address' in wwan: -        if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac): -            ip_type = 'ipv4v6' -        elif 'dhcpv6' in wwan['address'] or slaac: -            ip_type = 'ipv6' -        elif 'dhcp' in wwan['address']: -            ip_type = 'ipv4' - -    options = f'ip-type={ip_type},apn=' + wwan['apn'] -    if 'authentication' in wwan: -        options += ',user={user},password={password}'.format(**wwan['authentication']) - -    command = f'{base_cmd} --simple-connect="{options}"' -    call(command, stdout=DEVNULL) -    w.update(wwan) +    if 'shutdown_required' in wwan: +        ip_type = 'ipv4' +        slaac = dict_search('ipv6.address.autoconf', wwan) != None +        if 'address' in wwan: +            if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac): +                ip_type = 'ipv4v6' +            elif 'dhcpv6' in wwan['address'] or slaac: +                ip_type = 'ipv6' +            elif 'dhcp' in wwan['address']: +                ip_type = 'ipv4' -    if 'other_interfaces' not in wwan and 'deleted' in wwan: -        cmd(f'systemctl start {service_name}') +        options = f'ip-type={ip_type},apn=' + wwan['apn'] +        if 'authentication' in wwan: +            options += ',user={user},password={password}'.format(**wwan['authentication']) +        command = f'{base_cmd} --simple-connect="{options}"' +        call(command, stdout=DEVNULL) + +    w.update(wwan)      return None  if __name__ == '__main__': diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index 082c3e128..db8328259 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2017-2020 VyOS maintainers and contributors +# Copyright (C) 2017-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 @@ -15,19 +15,19 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import re -from copy import deepcopy  from sys import exit  from vyos.config import Config +from vyos.configdict import dict_merge  from vyos.validate import is_addr_assigned  from vyos.validate import is_loopback_addr  from vyos.version import get_version_data -from vyos import ConfigError  from vyos.util import call +from vyos.util import dict_search +from vyos.xml import defaults  from vyos.template import render - +from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -35,178 +35,73 @@ config_file = "/etc/default/lldpd"  vyos_config_file = "/etc/lldpd.d/01-vyos.conf"  base = ['service', 'lldp'] -default_config_data = { -    "options": '', -    "interface_list": '', -    "location": '' -} - -def get_options(config): -    options = {} -    config.set_level(base) - -    options['listen_vlan'] = config.exists('listen-vlan') -    options['mgmt_addr'] = [] -    for addr in config.return_values('management-address'): -        if is_addr_assigned(addr) and not is_loopback_addr(addr): -            options['mgmt_addr'].append(addr) -        else: -            message = 'WARNING: LLDP management address {0} invalid - '.format(addr) -            if is_loopback_addr(addr): -                message += '(loopback address).' -            else: -                message += 'address not found.' -            print(message) - -    snmp = config.exists('snmp enable') -    options["snmp"] = snmp -    if snmp: -        config.set_level('') -        options["sys_snmp"] = config.exists('service snmp') -        config.set_level(base) - -    config.set_level(base + ['legacy-protocols']) -    options['cdp'] = config.exists('cdp') -    options['edp'] = config.exists('edp') -    options['fdp'] = config.exists('fdp') -    options['sonmp'] = config.exists('sonmp') - -    # start with an unknown version information -    version_data = get_version_data() -    options['description'] = version_data['version'] -    options['listen_on'] = [] - -    return options - -def get_interface_list(config): -    config.set_level(base) -    intfs_names = config.list_nodes(['interface']) -    if len(intfs_names) < 0: -        return 0 - -    interface_list = [] -    for name in intfs_names: -        config.set_level(base + ['interface', name]) -        disable = config.exists(['disable']) -        intf = { -            'name': name, -            'disable': disable -        } -        interface_list.append(intf) -    return interface_list - - -def get_location_intf(config, name): -    path = base + ['interface', name] -    config.set_level(path) - -    config.set_level(path + ['location']) -    elin = '' -    coordinate_based = {} - -    if config.exists('elin'): -        elin = config.return_value('elin') - -    if config.exists('coordinate-based'): -        config.set_level(path + ['location', 'coordinate-based']) - -        coordinate_based['latitude'] = config.return_value(['latitude']) -        coordinate_based['longitude'] = config.return_value(['longitude']) - -        coordinate_based['altitude'] = '0' -        if config.exists(['altitude']): -            coordinate_based['altitude'] = config.return_value(['altitude']) - -        coordinate_based['datum'] = 'WGS84' -        if config.exists(['datum']): -            coordinate_based['datum'] = config.return_value(['datum']) - -    intf = { -        'name': name, -        'elin': elin, -        'coordinate_based': coordinate_based - -    } -    return intf - - -def get_location(config): -    config.set_level(base) -    intfs_names = config.list_nodes(['interface']) -    if len(intfs_names) < 0: -        return 0 - -    if config.exists('disable'): -        return 0 - -    intfs_location = [] -    for name in intfs_names: -        intf = get_location_intf(config, name) -        intfs_location.append(intf) - -    return intfs_location - -  def get_config(config=None): -    lldp = deepcopy(default_config_data)      if config:          conf = config      else:          conf = Config() +      if not conf.exists(base): -        return None -    else: -        lldp['options'] = get_options(conf) -        lldp['interface_list'] = get_interface_list(conf) -        lldp['location'] = get_location(conf) +        return {} -        return lldp +    lldp = conf.get_config_dict(base, key_mangling=('-', '_'), +                                get_first_key=True, no_tag_node_value_mangle=True) +    if conf.exists(['service', 'snmp']): +        lldp['system_snmp_enabled'] = '' + +    version_data = get_version_data() +    lldp['version'] = version_data['version'] + +    # We have gathered the dict representation of the CLI, but there are default +    # options which we need to update into the dictionary retrived. +    # location coordinates have a default value +    if 'interface' in lldp: +        for interface, interface_config in lldp['interface'].items(): +            default_values = defaults(base + ['interface']) +            if dict_search('location.coordinate_based', interface_config) == None: +                # no location specified - no need to add defaults +                del default_values['location']['coordinate_based']['datum'] +                del default_values['location']['coordinate_based']['altitude'] + +            # cleanup default_values dictionary from inner to outer +            # this might feel overkill here, but it does support easy extension +            # in the future with additional default values +            if len(default_values['location']['coordinate_based']) == 0: +                del default_values['location']['coordinate_based'] +            if len(default_values['location']) == 0: +                del default_values['location'] + +            lldp['interface'][interface] = dict_merge(default_values, +                                                   lldp['interface'][interface]) + +    return lldp  def verify(lldp):      # bail out early - looks like removal from running config      if lldp is None:          return -    # check location -    for location in lldp['location']: -        # check coordinate-based -        if len(location['coordinate_based']) > 0: -            # check longitude and latitude -            if not location['coordinate_based']['longitude']: -                raise ConfigError('Must define longitude for interface {0}'.format(location['name'])) - -            if not location['coordinate_based']['latitude']: -                raise ConfigError('Must define latitude for interface {0}'.format(location['name'])) - -            if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']): -                raise ConfigError('Invalid location for interface {0}:\n' \ -                                  'latitude should be a number followed by S or N'.format(location['name'])) - -            if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']): -                raise ConfigError('Invalid location for interface {0}:\n' \ -                                  'longitude should be a number followed by E or W'.format(location['name'])) - -            # check altitude and datum if exist -            if location['coordinate_based']['altitude']: -                if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']): -                    raise ConfigError('Invalid location for interface {0}:\n' \ -                                      'altitude should be a positive or negative number'.format(location['name'])) - -            if location['coordinate_based']['datum']: -                if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']): -                    raise ConfigError("Invalid location for interface {0}:\n' \ -                                      'datum should be WGS84, NAD83, or MLLW".format(location['name'])) - -        # check elin -        elif location['elin']: -            if not re.match(r'^[0-9]{10,25}$', location['elin']): -                raise ConfigError('Invalid location for interface {0}:\n' \ -                                  'ELIN number must be between 10-25 numbers'.format(location['name'])) +    if 'management_address' in lldp: +        for address in lldp['management_address']: +            message = f'WARNING: LLDP management address "{address}" is invalid' +            if is_loopback_addr(address): +                print(f'{message} - loopback address') +            elif not is_addr_assigned(address): +                print(f'{message} - not assigned to any interface') + +    if 'interface' in lldp: +        for interface, interface_config in lldp['interface'].items(): +            # bail out early if no location info present in interface config +            if 'location' not in interface_config: +                continue +            if 'coordinate_based' in interface_config['location']: +                if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']): +                    raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')      # check options -    if lldp['options']['snmp']: -        if not lldp['options']['sys_snmp']: +    if 'snmp' in lldp and 'enable' in lldp['snmp']: +        if 'system_snmp_enabled' not in lldp:              raise ConfigError('SNMP must be configured to enable LLDP SNMP') @@ -215,29 +110,17 @@ def generate(lldp):      if lldp is None:          return -    # generate listen on interfaces -    for intf in lldp['interface_list']: -        tmp = '' -        # add exclamation mark if interface is disabled -        if intf['disable']: -            tmp = '!' - -        tmp += intf['name'] -        lldp['options']['listen_on'].append(tmp) - -    # generate /etc/default/lldpd      render(config_file, 'lldp/lldpd.tmpl', lldp) -    # generate /etc/lldpd.d/01-vyos.conf      render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp) -  def apply(lldp): +    systemd_service = 'lldpd.service'      if lldp:          # start/restart lldp service -        call('systemctl restart lldpd.service') +        call(f'systemctl restart {systemd_service}')      else:          # LLDP service has been terminated -        call('systemctl stop lldpd.service') +        call(f'systemctl stop {systemd_service}')          if os.path.isfile(config_file):              os.unlink(config_file)          if os.path.isfile(vyos_config_file): diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 96f8f6fb6..9f319fc8a 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -28,6 +28,7 @@ from vyos.configdict import dict_merge  from vyos.template import render  from vyos.template import is_ip_network  from vyos.util import cmd +from vyos.util import run  from vyos.util import check_kmod  from vyos.util import dict_search  from vyos.validate import is_addr_assigned @@ -179,12 +180,19 @@ def verify(nat):      return None  def generate(nat): -    render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat, -           permission=0o755) +    render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat) + +    # dry-run newly generated configuration +    tmp = run(f'nft -c -f {nftables_nat_config}') +    if tmp > 0: +        if os.path.exists(nftables_ct_file): +            os.unlink(nftables_ct_file) +        raise ConfigError('Configuration file errors encountered!') +      return None  def apply(nat): -    cmd(f'{nftables_nat_config}') +    cmd(f'nft -f {nftables_nat_config}')      if os.path.isfile(nftables_nat_config):          os.unlink(nftables_nat_config) diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py index 539189442..3f834f55c 100755 --- a/src/conf_mode/policy-local-route.py +++ b/src/conf_mode/policy-local-route.py @@ -18,6 +18,7 @@ import os  from sys import exit +from netifaces import interfaces  from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.configdict import node_changed @@ -35,35 +36,92 @@ def get_config(config=None):          conf = config      else:          conf = Config() -    base = ['policy', 'local-route'] +    base = ['policy'] +      pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -    # delete policy local-route -    dict = {} -    tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_')) -    if tmp: -        for rule in (tmp or []): -            src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) -            fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark']) -            if src: -                dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) -                pbr.update(dict) -            if fwmk: -                dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict) +    for route in ['local_route', 'local_route6']: +        dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove' +        route_key = 'local-route' if route == 'local_route' else 'local-route6' +        base_rule = base + [route_key, 'rule'] + +        # delete policy local-route +        dict = {} +        tmp = node_changed(conf, base_rule, key_mangling=('-', '_')) +        if tmp: +            for rule in (tmp or []): +                src = leaf_node_changed(conf, base_rule + [rule, 'source']) +                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) +                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) +                dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) +                rule_def = {} +                if src: +                    rule_def = dict_merge({'source' : src}, rule_def) +                if fwmk: +                    rule_def = dict_merge({'fwmark' : fwmk}, rule_def) +                if iif: +                    rule_def = dict_merge({'inbound_interface' : iif}, rule_def) +                if dst: +                    rule_def = dict_merge({'destination' : dst}, rule_def) +                dict = dict_merge({dict_id : {rule : rule_def}}, dict)                  pbr.update(dict) -    # delete policy local-route rule x source x.x.x.x -    # delete policy local-route rule x fwmark x -    if 'rule' in pbr: -        for rule in pbr['rule']: -            src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) -            fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark']) -            if src: -                dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) -                pbr.update(dict) -            if fwmk: -                dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict) -                pbr.update(dict) +        if not route in pbr: +            continue + +        # delete policy local-route rule x source x.x.x.x +        # delete policy local-route rule x fwmark x +        # delete policy local-route rule x destination x.x.x.x +        if 'rule' in pbr[route]: +            for rule, rule_config in pbr[route]['rule'].items(): +                src = leaf_node_changed(conf, base_rule + [rule, 'source']) +                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) +                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) +                dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) +                # keep track of changes in configuration +                # otherwise we might remove an existing node although nothing else has changed +                changed = False + +                rule_def = {} +                # src is None if there are no changes to src +                if src is None: +                    # if src hasn't changed, include it in the removal selector +                    # if a new selector is added, we have to remove all previous rules without this selector +                    # to make sure we remove all previous rules with this source(s), it will be included +                    if 'source' in rule_config: +                        rule_def = dict_merge({'source': rule_config['source']}, rule_def) +                else: +                    # if src is not None, it's previous content will be returned +                    # this can be an empty array if it's just being set, or the previous value +                    # either way, something has to be changed and we only want to remove previous values +                    changed = True +                    # set the old value for removal if it's not empty +                    if len(src) > 0: +                        rule_def = dict_merge({'source' : src}, rule_def) +                if fwmk is None: +                    if 'fwmark' in rule_config: +                        rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def) +                else: +                    changed = True +                    if len(fwmk) > 0: +                        rule_def = dict_merge({'fwmark' : fwmk}, rule_def) +                if iif is None: +                    if 'inbound_interface' in rule_config: +                        rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def) +                else: +                    changed = True +                    if len(iif) > 0: +                        rule_def = dict_merge({'inbound_interface' : iif}, rule_def) +                if dst is None: +                    if 'destination' in rule_config: +                        rule_def = dict_merge({'destination': rule_config['destination']}, rule_def) +                else: +                    changed = True +                    if len(dst) > 0: +                        rule_def = dict_merge({'destination' : dst}, rule_def) +                if changed: +                    dict = dict_merge({dict_id : {rule : rule_def}}, dict) +                    pbr.update(dict)      return pbr @@ -72,13 +130,25 @@ def verify(pbr):      if not pbr:          return None -    if 'rule' in pbr: -        for rule in pbr['rule']: -            if 'source' not in pbr['rule'][rule] and 'fwmark' not in pbr['rule'][rule]: -                raise ConfigError('Source address or fwmark is required!') -            else: -                if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']: -                    raise ConfigError('Table set is required!') +    for route in ['local_route', 'local_route6']: +        if not route in pbr: +            continue + +        pbr_route = pbr[route] +        if 'rule' in pbr_route: +            for rule in pbr_route['rule']: +                if 'source' not in pbr_route['rule'][rule] \ +                        and 'destination' not in pbr_route['rule'][rule] \ +                        and 'fwmark' not in pbr_route['rule'][rule] \ +                        and 'inbound_interface' not in pbr_route['rule'][rule]: +                    raise ConfigError('Source or destination address or fwmark or inbound-interface is required!') +                else: +                    if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']: +                        raise ConfigError('Table set is required!') +                    if 'inbound_interface' in pbr_route['rule'][rule]: +                        interface = pbr_route['rule'][rule]['inbound_interface'] +                        if interface not in interfaces(): +                            raise ConfigError(f'Interface "{interface}" does not exist')      return None @@ -93,36 +163,51 @@ def apply(pbr):          return None      # Delete old rule if needed -    if 'rule_remove' in pbr: -        for rule in pbr['rule_remove']: -            if 'source' in pbr['rule_remove'][rule]: -                for src in pbr['rule_remove'][rule]['source']: -                    call(f'ip rule del prio {rule} from {src}') -            if 'fwmark' in  pbr['rule_remove'][rule]: -                for fwmk in pbr['rule_remove'][rule]['fwmark']: -                    call(f'ip rule del prio {rule} from all fwmark {fwmk}') +    for rule_rm in ['rule_remove', 'rule6_remove']: +        if rule_rm in pbr: +            v6 = " -6" if rule_rm == 'rule6_remove' else "" +            for rule, rule_config in pbr[rule_rm].items(): +                rule_config['source'] = rule_config['source'] if 'source' in rule_config else [''] +                for src in rule_config['source']: +                    f_src = '' if src == '' else f' from {src} ' +                    rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else [''] +                    for dst in rule_config['destination']: +                        f_dst = '' if dst == '' else f' to {dst} ' +                        rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else [''] +                        for fwmk in rule_config['fwmark']: +                            f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} ' +                            rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else [''] +                            for iif in rule_config['inbound_interface']: +                                f_iif = '' if iif == '' else f' iif {iif} ' +                                call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}')      # Generate new config -    if 'rule' in pbr: -        for rule in pbr['rule']: -            table = pbr['rule'][rule]['set']['table'] -            # Only source in the rule -            # set policy local-route rule 100 source '203.0.113.1' -            if 'source' in pbr['rule'][rule] and not 'fwmark' in pbr['rule'][rule]: -                for src in pbr['rule'][rule]['source']: -                    call(f'ip rule add prio {rule} from {src} lookup {table}') -            # Only fwmark in the rule -            # set policy local-route rule 101 fwmark '23' -            if 'fwmark' in pbr['rule'][rule] and not 'source' in pbr['rule'][rule]: -                fwmk = pbr['rule'][rule]['fwmark'] -                call(f'ip rule add prio {rule} from all fwmark {fwmk} lookup {table}') -            # Source and fwmark in the rule -            # set policy local-route rule 100 source '203.0.113.1' -            # set policy local-route rule 100 fwmark '23' -            if 'source' in pbr['rule'][rule] and 'fwmark' in pbr['rule'][rule]: -                fwmk = pbr['rule'][rule]['fwmark'] -                for src in pbr['rule'][rule]['source']: -                    call(f'ip rule add prio {rule} from {src} fwmark {fwmk} lookup {table}') +    for route in ['local_route', 'local_route6']: +        if not route in pbr: +            continue + +        v6 = " -6" if route == 'local_route6' else "" + +        pbr_route = pbr[route] +        if 'rule' in pbr_route: +            for rule, rule_config in pbr_route['rule'].items(): +                table = rule_config['set']['table'] + +                rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['all'] +                for src in rule_config['source'] or ['all']: +                    f_src = '' if src == '' else f' from {src} ' +                    rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['all'] +                    for dst in rule_config['destination']: +                        f_dst = '' if dst == '' else f' to {dst} ' +                        f_fwmk = '' +                        if 'fwmark' in rule_config: +                            fwmk = rule_config['fwmark'] +                            f_fwmk = f' fwmark {fwmk} ' +                        f_iif = '' +                        if 'inbound_interface' in rule_config: +                            iif = rule_config['inbound_interface'] +                            f_iif = f' iif {iif} ' +                        call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}')      return None diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py index e81135a74..1108aebe6 100755 --- a/src/conf_mode/policy-route-interface.py +++ b/src/conf_mode/policy-route-interface.py @@ -52,7 +52,7 @@ def verify(if_policy):      if not if_policy:          return None -    for route in ['route', 'ipv6_route']: +    for route in ['route', 'route6']:          if route in if_policy:              if route not in if_policy['policy']:                  raise ConfigError('Policy route not configured') @@ -71,7 +71,7 @@ def cleanup_rule(table, chain, ifname, new_name=None):      results = cmd(f'nft -a list chain {table} {chain}').split("\n")      retval = None      for line in results: -        if f'oifname "{ifname}"' in line: +        if f'ifname "{ifname}"' in line:              if new_name and f'jump {new_name}' in line:                  # new_name is used to clear rules for any previously referenced chains                  # returns true when rule exists and doesn't need to be created @@ -98,8 +98,8 @@ def apply(if_policy):      else:          cleanup_rule('ip mangle', route_chain, ifname) -    if 'ipv6_route' in if_policy: -        name = 'VYOS_PBR6_' + if_policy['ipv6_route'] +    if 'route6' in if_policy: +        name = 'VYOS_PBR6_' + if_policy['route6']          rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name)          if not rule_exists: diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index d098be68d..3d1d7d8c5 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os +import re  from json import loads  from sys import exit @@ -31,6 +32,35 @@ airbag.enable()  mark_offset = 0x7FFFFFFF  nftables_conf = '/run/nftables_policy.conf' +preserve_chains = [ +    'VYOS_PBR_PREROUTING', +    'VYOS_PBR_POSTROUTING', +    'VYOS_PBR6_PREROUTING', +    'VYOS_PBR6_POSTROUTING' +] + +valid_groups = [ +    'address_group', +    'network_group', +    'port_group' +] + +def get_policy_interfaces(conf): +    out = {} +    interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, +                                    no_tag_node_value_mangle=True) +    def find_interfaces(iftype_conf, output={}, prefix=''): +        for ifname, if_conf in iftype_conf.items(): +            if 'policy' in if_conf: +                output[prefix + ifname] = if_conf['policy'] +            for vif in ['vif', 'vif_s', 'vif_c']: +                if vif in if_conf: +                    output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) +        return output +    for iftype, iftype_conf in interfaces.items(): +        out.update(find_interfaces(iftype_conf)) +    return out +  def get_config(config=None):      if config:          conf = config @@ -38,68 +68,149 @@ def get_config(config=None):          conf = Config()      base = ['policy'] -    if not conf.exists(base + ['route']) and not conf.exists(base + ['ipv6-route']): -        return None -      policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,                                      no_tag_node_value_mangle=True) +    policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, +                                    no_tag_node_value_mangle=True) +    policy['interfaces'] = get_policy_interfaces(conf) +      return policy -def verify(policy): -    # bail out early - looks like removal from running config -    if not policy: -        return None +def verify_rule(policy, name, rule_conf, ipv6): +    icmp = 'icmp' if not ipv6 else 'icmpv6' +    if icmp in rule_conf: +        icmp_defined = False +        if 'type_name' in rule_conf[icmp]: +            icmp_defined = True +            if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]: +                raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name') +        if 'code' in rule_conf[icmp]: +            icmp_defined = True +            if 'type' not in rule_conf[icmp]: +                raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined') +        if 'type' in rule_conf[icmp]: +            icmp_defined = True + +        if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp: +            raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP') + +    if 'set' in rule_conf: +        if 'tcp_mss' in rule_conf['set']: +            tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') +            if not tcp_flags or 'syn' not in tcp_flags: +                raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS') + +    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') +    if tcp_flags: +        if dict_search_args(rule_conf, 'protocol') != 'tcp': +            raise ConfigError('Protocol must be tcp when specifying tcp flags') + +        not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') +        if not_flags: +            duplicates = [flag for flag in tcp_flags if flag in not_flags] +            if duplicates: +                raise ConfigError(f'Cannot match a tcp flag as set and not set') + +    for side in ['destination', 'source']: +        if side in rule_conf: +            side_conf = rule_conf[side] + +            if 'group' in side_conf: +                if {'address_group', 'network_group'} <= set(side_conf['group']): +                    raise ConfigError('Only one address-group or network-group can be specified') + +                for group in valid_groups: +                    if group in side_conf['group']: +                        group_name = side_conf['group'][group] + +                        if group_name.startswith('!'): +                            group_name = group_name[1:] + +                        fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group +                        error_group = fw_group.replace("_", "-") +                        group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name) -    for route in ['route', 'ipv6_route']: +                        if group_obj is None: +                            raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule') + +                        if not group_obj: +                            print(f'WARNING: {error_group} "{group_name}" has no members') + +            if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): +                if 'protocol' not in rule_conf: +                    raise ConfigError('Protocol must be defined if specifying a port or port-group') + +                if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: +                    raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') + +def verify(policy): +    for route in ['route', 'route6']: +        ipv6 = route == 'route6'          if route in policy:              for name, pol_conf in policy[route].items():                  if 'rule' in pol_conf: -                    for rule_id, rule_conf in pol_conf.items(): -                        icmp = 'icmp' if route == 'route' else 'icmpv6' -                        if icmp in rule_conf: -                            icmp_defined = False -                            if 'type_name' in rule_conf[icmp]: -                                icmp_defined = True -                                if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]: -                                    raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name') -                            if 'code' in rule_conf[icmp]: -                                icmp_defined = True -                                if 'type' not in rule_conf[icmp]: -                                    raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined') -                            if 'type' in rule_conf[icmp]: -                                icmp_defined = True - -                            if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp: -                                raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP') -                        if 'set' in rule_conf: -                            if 'tcp_mss' in rule_conf['set']: -                                tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') -                                if not tcp_flags or 'SYN' not in tcp_flags.split(","): -                                    raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS') -                        if 'tcp' in rule_conf: -                            if 'flags' in rule_conf['tcp']: -                                if 'protocol' not in rule_conf or rule_conf['protocol'] != 'tcp': -                                    raise ConfigError(f'{name} rule {rule_id}: TCP flags can only be set if protocol is set to TCP') +                    for rule_id, rule_conf in pol_conf['rule'].items(): +                        verify_rule(policy, name, rule_conf, ipv6) + +    for ifname, if_policy in policy['interfaces'].items(): +        name = dict_search_args(if_policy, 'route') +        ipv6_name = dict_search_args(if_policy, 'route6') +        if name and not dict_search_args(policy, 'route', name): +            raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}') + +        if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name): +            raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}')      return None -def generate(policy): -    if not policy: -        if os.path.exists(nftables_conf): -            os.unlink(nftables_conf) -        return None +def cleanup_rule(table, jump_chain): +    commands = [] +    results = cmd(f'nft -a list table {table}').split("\n") +    for line in results: +        if f'jump {jump_chain}' in line: +            handle_search = re.search('handle (\d+)', line) +            if handle_search: +                commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') +    return commands +def cleanup_commands(policy): +    commands = [] +    for table in ['ip mangle', 'ip6 mangle']: +        json_str = cmd(f'nft -j list table {table}') +        obj = loads(json_str) +        if 'nftables' not in obj: +            continue +        for item in obj['nftables']: +            if 'chain' in item: +                chain = item['chain']['name'] +                if not chain.startswith("VYOS_PBR"): +                    continue +                if chain not in preserve_chains: +                    if table == 'ip mangle' and dict_search_args(policy, 'route', chain.replace("VYOS_PBR_", "", 1)): +                        commands.append(f'flush chain {table} {chain}') +                    elif table == 'ip6 mangle' and dict_search_args(policy, 'route6', chain.replace("VYOS_PBR6_", "", 1)): +                        commands.append(f'flush chain {table} {chain}') +                    else: +                        commands += cleanup_rule(table, chain) +                        commands.append(f'delete chain {table} {chain}') +    return commands + +def generate(policy):      if not os.path.exists(nftables_conf):          policy['first_install'] = True +    else: +        policy['cleanup_commands'] = cleanup_commands(policy)      render(nftables_conf, 'firewall/nftables-policy.tmpl', policy)      return None  def apply_table_marks(policy): -    for route in ['route', 'ipv6_route']: +    for route in ['route', 'route6']:          if route in policy: +            cmd_str = 'ip' if route == 'route' else 'ip -6' +            tables = []              for name, pol_conf in policy[route].items():                  if 'rule' in pol_conf:                      for rule_id, rule_conf in pol_conf['rule'].items(): @@ -107,31 +218,27 @@ def apply_table_marks(policy):                          if set_table:                              if set_table == 'main':                                  set_table = '254' +                            if set_table in tables: +                                continue +                            tables.append(set_table)                              table_mark = mark_offset - int(set_table) -                            cmd(f'ip rule add fwmark {table_mark} table {set_table}') +                            cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')  def cleanup_table_marks(): -    json_rules = cmd('ip -j -N rule list') -    rules = loads(json_rules) -    for rule in rules: -        if 'fwmark' not in rule or 'table' not in rule: -            continue -        fwmark = rule['fwmark'] -        table = int(rule['table']) -        if fwmark[:2] == '0x': -            fwmark = int(fwmark, 16) -        if (int(fwmark) == (mark_offset - table)): -            cmd(f'ip rule del fwmark {fwmark} table {table}') +    for cmd_str in ['ip', 'ip -6']: +        json_rules = cmd(f'{cmd_str} -j -N rule list') +        rules = loads(json_rules) +        for rule in rules: +            if 'fwmark' not in rule or 'table' not in rule: +                continue +            fwmark = rule['fwmark'] +            table = int(rule['table']) +            if fwmark[:2] == '0x': +                fwmark = int(fwmark, 16) +            if (int(fwmark) == (mark_offset - table)): +                cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}')  def apply(policy): -    if not policy or 'first_install' not in policy: -        run(f'nft flush table ip mangle') -        run(f'nft flush table ip6 mangle') - -    if not policy: -        cleanup_table_marks() -        return None -      install_result = run(f'nft -f {nftables_conf}')      if install_result == 1:          raise ConfigError('Failed to apply policy based routing') diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index e251396c7..9d8fcfa36 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -87,6 +87,7 @@ def verify(policy):              # human readable instance name (hypen instead of underscore)              policy_hr = policy_type.replace('_', '-') +            entries = []              for rule, rule_config in instance_config['rule'].items():                  mandatory_error = f'must be specified for "{policy_hr} {instance} rule {rule}"!'                  if 'action' not in rule_config: @@ -113,6 +114,10 @@ def verify(policy):                      if 'prefix' not in rule_config:                          raise ConfigError(f'A prefix {mandatory_error}') +                    if rule_config in entries: +                        raise ConfigError(f'Rule "{rule}" contains a duplicate prefix definition!') +                    entries.append(rule_config) +      # route-maps tend to be a bit more complex so they get their own verify() section      if 'route_map' in policy: diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index d8704727c..dace53d37 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -159,13 +159,21 @@ def verify(bgp):                  # Only checks for ipv4 and ipv6 neighbors                  # Check if neighbor address is assigned as system interface address -                if is_ip(peer) and is_addr_assigned(peer): -                    raise ConfigError(f'Can not configure a local address as neighbor "{peer}"') +                vrf = None +                vrf_error_msg = f' in default VRF!' +                if 'vrf' in bgp: +                    vrf = bgp['vrf'] +                    vrf_error_msg = f' in VRF "{vrf}"!' + +                if is_ip(peer) and is_addr_assigned(peer, vrf): +                    raise ConfigError(f'Can not configure local address as neighbor "{peer}"{vrf_error_msg}')                  elif is_interface(peer):                      if 'peer_group' in peer_config:                          raise ConfigError(f'peer-group must be set under the interface node of "{peer}"')                      if 'remote_as' in peer_config:                          raise ConfigError(f'remote-as must be set under the interface node of "{peer}"') +                    if 'source_interface' in peer_config['interface']: +                        raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')              for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',                          'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec', @@ -205,6 +213,11 @@ def verify(bgp):                      if 'non_exist_map' in afi_config['conditionally_advertise']:                          verify_route_map(afi_config['conditionally_advertise']['non_exist_map'], bgp) +                # T4332: bgp deterministic-med cannot be disabled while addpath-tx-bestpath-per-AS is in use +                if 'addpath_tx_per_as' in afi_config: +                    if dict_search('parameters.deterministic_med', bgp) == None: +                        raise ConfigError('addpath-tx-per-as requires BGP deterministic-med paramtere to be set!') +                  # Validate if configured Prefix list exists                  if 'prefix_list' in afi_config:                      for tmp in ['import', 'export']: diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 9b4b215de..f2501e38a 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -169,28 +169,40 @@ def verify(isis):      # Segment routing checks      if dict_search('segment_routing.global_block', isis): -        high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) -        low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) +        g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) +        g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) -        # If segment routing global block high value is blank, throw error -        if (low_label_value and not high_label_value) or (high_label_value and not low_label_value): -            raise ConfigError('Segment routing global block requires both low and high value!') +        # If segment routing global block high or low value is blank, throw error +        if not (g_low_label_value or g_high_label_value): +            raise ConfigError('Segment routing global-block requires both low and high value!')          # If segment routing global block low value is higher than the high value, throw error -        if int(low_label_value) > int(high_label_value): -            raise ConfigError('Segment routing global block low value must be lower than high value') +        if int(g_low_label_value) > int(g_high_label_value): +            raise ConfigError('Segment routing global-block low value must be lower than high value')      if dict_search('segment_routing.local_block', isis): -        high_label_value = dict_search('segment_routing.local_block.high_label_value', isis) -        low_label_value = dict_search('segment_routing.local_block.low_label_value', isis) +        if dict_search('segment_routing.global_block', isis) == None: +            raise ConfigError('Segment routing local-block requires global-block to be configured!') -        # If segment routing local block high value is blank, throw error -        if (low_label_value and not high_label_value) or (high_label_value and not low_label_value): -            raise ConfigError('Segment routing local block requires both high and low value!') +        l_high_label_value = dict_search('segment_routing.local_block.high_label_value', isis) +        l_low_label_value = dict_search('segment_routing.local_block.low_label_value', isis) -        # If segment routing local block low value is higher than the high value, throw error -        if int(low_label_value) > int(high_label_value): -            raise ConfigError('Segment routing local block low value must be lower than high value') +        # If segment routing local-block high or low value is blank, throw error +        if not (l_low_label_value or l_high_label_value): +            raise ConfigError('Segment routing local-block requires both high and low value!') + +        # If segment routing local-block low value is higher than the high value, throw error +        if int(l_low_label_value) > int(l_high_label_value): +            raise ConfigError('Segment routing local-block low value must be lower than high value') + +        # local-block most live outside global block +        global_range = range(int(g_low_label_value), int(g_high_label_value) +1) +        local_range  = range(int(l_low_label_value), int(l_high_label_value) +1) + +        # Check for overlapping ranges +        if list(set(global_range) & set(local_range)): +            raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ +                              f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')      return None diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 0b0c7d07b..933e23065 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -20,11 +20,10 @@ from sys import exit  from glob import glob  from vyos.config import Config -from vyos.configdict import node_changed  from vyos.template import render_to_string -from vyos.util import call  from vyos.util import dict_search  from vyos.util import read_file +from vyos.util import sysctl_write  from vyos import ConfigError  from vyos import frr  from vyos import airbag @@ -89,21 +88,21 @@ def apply(mpls):      labels = '0'      if 'interface' in mpls:          labels = '1048575' -    call(f'sysctl -wq net.mpls.platform_labels={labels}') +    sysctl_write('net.mpls.platform_labels', labels)      # Check for changes in global MPLS options      if 'parameters' in mpls:              # Choose whether to copy IP TTL to MPLS header TTL          if 'no_propagate_ttl' in mpls['parameters']: -            call('sysctl -wq net.mpls.ip_ttl_propagate=0') +            sysctl_write('net.mpls.ip_ttl_propagate', 0)              # Choose whether to limit maximum MPLS header TTL          if 'maximum_ttl' in mpls['parameters']:              ttl = mpls['parameters']['maximum_ttl'] -            call(f'sysctl -wq net.mpls.default_ttl={ttl}') +            sysctl_write('net.mpls.default_ttl', ttl)      else:          # Set default global MPLS options if not defined. -        call('sysctl -wq net.mpls.ip_ttl_propagate=1') -        call('sysctl -wq net.mpls.default_ttl=255') +        sysctl_write('net.mpls.ip_ttl_propagate', 1) +        sysctl_write('net.mpls.default_ttl', 255)      # Enable and disable MPLS processing on interfaces per configuration      if 'interface' in mpls: @@ -117,11 +116,11 @@ def apply(mpls):              if '1' in interface_state:                  if system_interface not in mpls['interface']:                      system_interface = system_interface.replace('.', '/') -                    call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0') +                    sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)              elif '0' in interface_state:                  if system_interface in mpls['interface']:                      system_interface = system_interface.replace('.', '/') -                    call(f'sysctl -wq net.mpls.conf.{system_interface}.input=1') +                    sysctl_write(f'net.mpls.conf.{system_interface}.input', 1)      else:          system_interfaces = []          # If MPLS interfaces are not configured, set MPLS processing disabled @@ -129,7 +128,7 @@ def apply(mpls):              system_interfaces.append(os.path.basename(interface))          for system_interface in system_interfaces:              system_interface = system_interface.replace('.', '/') -            call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0') +            sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)      return None diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 4895cde6f..26d491838 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -25,6 +25,7 @@ from vyos.configdict import node_changed  from vyos.configverify import verify_common_route_maps  from vyos.configverify import verify_route_map  from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_access_list  from vyos.template import render_to_string  from vyos.util import dict_search  from vyos.util import get_interface_config @@ -159,6 +160,16 @@ def verify(ospf):      route_map_name = dict_search('default_information.originate.route_map', ospf)      if route_map_name: verify_route_map(route_map_name, ospf) +    # Validate if configured Access-list exists  +    if 'area' in ospf: +          for area, area_config in ospf['area'].items(): +              if 'import_list' in area_config: +                  acl_import = area_config['import_list'] +                  if acl_import: verify_access_list(acl_import, ospf) +              if 'export_list' in area_config: +                  acl_export = area_config['export_list'] +                  if acl_export: verify_access_list(acl_export, ospf) +      if 'interface' in ospf:          for interface, interface_config in ospf['interface'].items():              verify_interface_exists(interface) diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index c1e427b16..f0ec48de4 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -82,6 +82,10 @@ def verify(static):                      for interface, interface_config in prefix_options[type].items():                          verify_vrf(interface_config) +            if {'blackhole', 'reject'} <= set(prefix_options): +                raise ConfigError(f'Can not use both blackhole and reject for '\ +                                  'prefix "{prefix}"!') +      return None  def generate(static): diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py new file mode 100755 index 000000000..dbe3be225 --- /dev/null +++ b/src/conf_mode/qos.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() +    base = ['qos'] +    if not conf.exists(base): +        return None + +    qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + +    if 'policy' in qos: +        for policy in qos['policy']: +            # CLI mangles - to _ for better Jinja2 compatibility - do we need +            # Jinja2 here? +            policy = policy.replace('-','_') + +            default_values = defaults(base + ['policy', policy]) + +            # class is another tag node which requires individual handling +            class_default_values = defaults(base + ['policy', policy, 'class']) +            if 'class' in default_values: +                del default_values['class'] + +            for p_name, p_config in qos['policy'][policy].items(): +                qos['policy'][policy][p_name] = dict_merge( +                    default_values, qos['policy'][policy][p_name]) + +                if 'class' in p_config: +                    for p_class in p_config['class']: +                        qos['policy'][policy][p_name]['class'][p_class] = dict_merge( +                            class_default_values, qos['policy'][policy][p_name]['class'][p_class]) + +    import pprint +    pprint.pprint(qos) +    return qos + +def verify(qos): +    if not qos: +        return None + +    # network policy emulator +    # reorder rerquires delay to be set + +    raise ConfigError('123') +    return None + +def generate(qos): +    return None + +def apply(qos): +    return None + +if __name__ == '__main__': +    try: +        c = get_config() +        verify(c) +        generate(c) +        apply(c) +    except ConfigError as e: +        print(e) +        exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index f676fdbbe..2ebee8018 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -41,6 +41,7 @@ default_config_data = {      'interfaces': [],      'dnsv4': [],      'dnsv6': [], +    'client_named_ip_pool': [],      'client_ipv6_pool': [],      'client_ipv6_delegate_prefix': [],      'radius_server': [], @@ -219,6 +220,22 @@ def get_config(config=None):      conf.set_level(base_path) +    # Named client-ip-pool +    if conf.exists(['client-ip-pool', 'name']): +        for name in conf.list_nodes(['client-ip-pool', 'name']): +            tmp = { +                'name': name, +                'gateway_address': '', +                'subnet': '' +            } + +            if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']): +                tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address']) +            if conf.exists(['client-ip-pool', 'name', name, 'subnet']): +                tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet']) + +            ipoe['client_named_ip_pool'].append(tmp) +      if conf.exists(['client-ipv6-pool', 'prefix']):          for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):              tmp = { @@ -254,10 +271,6 @@ def verify(ipoe):      if not ipoe['interfaces']:          raise ConfigError('No IPoE interface configured') -    for interface in ipoe['interfaces']: -        if not interface['range']: -            raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"') -      if len(ipoe['dnsv4']) > 2:          raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index a1e7a7286..8a972b9fe 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -22,6 +22,7 @@ from shutil import rmtree  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.ifconfig import Section  from vyos.template import render  from vyos.util import call  from vyos.util import chown @@ -42,6 +43,24 @@ 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 +  def get_nft_filter_chains():      """      Get nft chains for table filter @@ -57,6 +76,7 @@ def get_nft_filter_chains():      return chain_list +  def get_config(config=None):      if config: @@ -75,8 +95,9 @@ def get_config(config=None):      default_values = defaults(base)      monitoring = dict_merge(default_values, monitoring) -    monitoring['nft_chains'] = get_nft_filter_chains()      monitoring['custom_scripts_dir'] = custom_scripts_dir +    monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) +    monitoring['nft_chains'] = get_nft_filter_chains()      return monitoring diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py new file mode 100755 index 000000000..d21b31990 --- /dev/null +++ b/src/conf_mode/service_upnp.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +import uuid +import netifaces +from ipaddress import IPv4Network +from ipaddress import IPv6Network + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_vrf +from vyos.util import call +from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/upnp/miniupnp.conf' + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() + +    base = ['service', 'upnp'] +    upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + +    if not upnpd: +        return None + +    if 'rule' in upnpd: +        default_member_values = defaults(base + ['rule']) +        for rule,rule_config in upnpd['rule'].items(): +            upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule]) + +    uuidgen = uuid.uuid1() +    upnpd.update({'uuid': uuidgen}) + +    return upnpd + +def get_all_interface_addr(prefix, filter_dev, filter_family): +    list_addr = [] +    interfaces = netifaces.interfaces() + +    for interface in interfaces: +        if filter_dev and interface in filter_dev: +            continue +        addrs = netifaces.ifaddresses(interface) +        if netifaces.AF_INET in addrs.keys(): +            if netifaces.AF_INET in filter_family: +                for addr in addrs[netifaces.AF_INET]: +                    if prefix: +                        # we need to manually assemble a list of IPv4 address/prefix +                        prefix = '/' + \ +                            str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen) +                        list_addr.append(addr['addr'] + prefix) +                    else: +                        list_addr.append(addr['addr']) +        if netifaces.AF_INET6 in addrs.keys(): +            if netifaces.AF_INET6 in filter_family: +                for addr in addrs[netifaces.AF_INET6]: +                    if prefix: +                        # we need to manually assemble a list of IPv4 address/prefix +                        bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1') +                        prefix = '/' + str(bits) +                        list_addr.append(addr['addr'] + prefix) +                    else: +                        list_addr.append(addr['addr']) + +    return list_addr + +def verify(upnpd): +    if not upnpd: +        return None + +    if 'wan_interface' not in upnpd: +        raise ConfigError('To enable UPNP, you must have the "wan-interface" option!') + +    if 'rule' in upnpd: +        for rule, rule_config in upnpd['rule'].items(): +            for option in ['external_port_range', 'internal_port_range', 'ip', 'action']: +                if option not in rule_config: +                    tmp = option.replace('_', '-') +                    raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!') + +    if 'stun' in upnpd: +        for option in ['host', 'port']: +            if option not in upnpd['stun']: +                raise ConfigError(f'A UPNP stun support must have an "{option}" option!') + +    # Check the validity of the IP address +    listen_dev = [] +    system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6]) +    system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6]) +    for listen_if_or_addr in upnpd['listen']: +        if listen_if_or_addr not in netifaces.interfaces(): +            listen_dev.append(listen_if_or_addr) +        if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and (listen_if_or_addr not in netifaces.interfaces()): +            if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast: +                raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') +            if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast: +                raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + +    system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6]) +    system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6]) +    for listen_if_or_addr in upnpd['listen']: +        if listen_if_or_addr not in netifaces.interfaces() and (listen_if_or_addr not in system_listening_dev_addrs_cidr) and (listen_if_or_addr not in system_listening_dev_addrs) and is_ipv6(listen_if_or_addr) and (not IPv6Network(listen_if_or_addr).is_multicast): +            raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card') + +def generate(upnpd): +    if not upnpd: +        return None + +    if os.path.isfile(config_file): +        os.unlink(config_file) + +    render(config_file, 'firewall/upnpd.conf.tmpl', upnpd) + +def apply(upnpd): +    systemd_service_name = 'miniupnpd.service' +    if not upnpd: +        # Stop the UPNP service +        call(f'systemctl stop {systemd_service_name}') +    else: +        # Start the UPNP service +        call(f'systemctl restart {systemd_service_name}') + +if __name__ == '__main__': +    try: +        c = get_config() +        verify(c) +        generate(c) +        apply(c) +    except ConfigError as e: +        print(e) +        exit(1) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 32cb2f036..05fc3a97a 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.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 @@ -20,14 +20,13 @@ from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.util import call  from vyos.util import dict_search +from vyos.util import sysctl_write +from vyos.util import write_file  from vyos.xml import defaults  from vyos import ConfigError  from vyos import airbag  airbag.enable() -def sysctl(name, value): -    call(f'sysctl -wq {name}={value}') -  def get_config(config=None):      if config:          conf = config @@ -50,29 +49,29 @@ def generate(opt):      pass  def apply(opt): +    # Apply ARP threshold values +    # table_size has a default value - thus the key always exists      size = int(dict_search('arp.table_size', opt)) -    if size: -        # apply ARP threshold values -        sysctl('net.ipv4.neigh.default.gc_thresh3', str(size)) -        sysctl('net.ipv4.neigh.default.gc_thresh2', str(size // 2)) -        sysctl('net.ipv4.neigh.default.gc_thresh1', str(size // 8)) +    # Amount upon reaching which the records begin to be cleared immediately +    sysctl_write('net.ipv4.neigh.default.gc_thresh3', size) +    # Amount after which the records begin to be cleaned after 5 seconds +    sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2) +    # Minimum number of stored records is indicated which is not cleared +    sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)      # enable/disable IPv4 forwarding -    tmp = '1' -    if 'disable_forwarding' in opt: -        tmp = '0' -    sysctl('net.ipv4.conf.all.forwarding', tmp) +    tmp = dict_search('disable_forwarding', opt) +    value = '0' if (tmp != None) else '1' +    write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) -    tmp = '0' -    # configure multipath - dict_search() returns an empty dict if key was found -    if isinstance(dict_search('multipath.ignore_unreachable_nexthops', opt), dict): -        tmp = '1' -    sysctl('net.ipv4.fib_multipath_use_neigh', tmp) +    # configure multipath +    tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) +    value = '1' if (tmp != None) else '0' +    sysctl_write('net.ipv4.fib_multipath_use_neigh', value) -    tmp = '0' -    if isinstance(dict_search('multipath.layer4_hashing', opt), dict): -        tmp = '1' -    sysctl('net.ipv4.fib_multipath_hash_policy', tmp) +    tmp = dict_search('multipath.layer4_hashing', opt) +    value = '1' if (tmp != None) else '0' +    sysctl_write('net.ipv4.fib_multipath_hash_policy', value)  if __name__ == '__main__':      try: diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index f70ec2631..26aacf46b 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019 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 @@ -15,95 +15,68 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import sys  from sys import exit -from copy import deepcopy  from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import dict_search +from vyos.util import sysctl_write +from vyos.util import write_file +from vyos.xml import defaults  from vyos import ConfigError -from vyos.util import call -  from vyos import airbag  airbag.enable() -ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf' - -default_config_data = { -    'reboot_message': False, -    'ipv6_forward': '1', -    'disable_addr_assignment': False, -    'mp_layer4_hashing': '0', -    'neighbor_cache': 8192, -    'strict_dad': '1' - -} - -def sysctl(name, value): -    call('sysctl -wq {}={}'.format(name, value)) -  def get_config(config=None): -    ip_opt = deepcopy(default_config_data)      if config:          conf = config      else:          conf = Config() -    conf.set_level('system ipv6') -    if conf.exists(''): -        ip_opt['disable_addr_assignment'] = conf.exists('disable') -        if conf.exists_effective('disable') != conf.exists('disable'): -            ip_opt['reboot_message'] = True - -        if conf.exists('disable-forwarding'): -            ip_opt['ipv6_forward'] = '0' +    base = ['system', 'ipv6'] -        if conf.exists('multipath layer4-hashing'): -            ip_opt['mp_layer4_hashing'] = '1' +    opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) -        if conf.exists('neighbor table-size'): -            ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size')) +    # 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) +    opt = dict_merge(default_values, opt) -        if conf.exists('strict-dad'): -            ip_opt['strict_dad'] = 2 +    return opt -    return ip_opt - -def verify(ip_opt): +def verify(opt):      pass -def generate(ip_opt): +def generate(opt):      pass -def apply(ip_opt): -    # disable IPv6 address assignment -    if ip_opt['disable_addr_assignment']: -        with open(ipv6_disable_file, 'w') as f: -            f.write('options ipv6 disable_ipv6=1') -    else: -        if os.path.exists(ipv6_disable_file): -            os.unlink(ipv6_disable_file) - -    if ip_opt['reboot_message']: -        print('Changing IPv6 disable parameter will only take affect\n' \ -              'when the system is rebooted.') - +def apply(opt):      # configure multipath -    sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing']) - -    # apply neighbor table threshold values -    sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache']) -    sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2) -    sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8) +    tmp = dict_search('multipath.layer4_hashing', opt) +    value = '1' if (tmp != None) else '0' +    sysctl_write('net.ipv6.fib_multipath_hash_policy', value) + +    # Apply ND threshold values +    # table_size has a default value - thus the key always exists +    size = int(dict_search('neighbor.table_size', opt)) +    # Amount upon reaching which the records begin to be cleared immediately +    sysctl_write('net.ipv6.neigh.default.gc_thresh3', size) +    # Amount after which the records begin to be cleaned after 5 seconds +    sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2) +    # Minimum number of stored records is indicated which is not cleared +    sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)      # enable/disable IPv6 forwarding -    with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f: -        f.write(ip_opt['ipv6_forward']) +    tmp = dict_search('disable_forwarding', opt) +    value = '0' if (tmp != None) else '1' +    write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)      # configure IPv6 strict-dad +    tmp = dict_search('strict_dad', opt) +    value = '2' if (tmp != None) else '1'      for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):          for name in files: -            if name == "accept_dad": -                with open(os.path.join(root, name), 'w') as f: -                    f.write(str(ip_opt['strict_dad'])) +            if name == 'accept_dad': +                write_file(os.path.join(root, name), value)  if __name__ == '__main__':      try: diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 4dd7f936d..c9c6aa187 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -23,6 +23,7 @@ from pwd import getpwall  from pwd import getpwnam  from spwd import getspnam  from sys import exit +from time import sleep  from vyos.config import Config  from vyos.configdict import dict_merge @@ -31,6 +32,7 @@ from vyos.template import render  from vyos.template import is_ipv4  from vyos.util import cmd  from vyos.util import call +from vyos.util import run  from vyos.util import DEVNULL  from vyos.util import dict_search  from vyos.xml import defaults @@ -250,13 +252,22 @@ def apply(login):      if 'rm_users' in login:          for user in login['rm_users']:              try: +                # Disable user to prevent re-login +                call(f'usermod -s /sbin/nologin {user}') +                  # Logout user if he is still logged in                  if user in list(set([tmp[0] for tmp in users()])):                      print(f'{user} is logged in, forcing logout!') -                    call(f'pkill -HUP -u {user}') - -                # Remove user account but leave home directory to be safe -                call(f'userdel --remove {user}', stderr=DEVNULL) +                    # re-run command until user is logged out +                    while run(f'pkill -HUP -u {user}'): +                        sleep(0.250) + +                # Remove user account but leave home directory in place. Re-run +                # command until user is removed - userdel might return 8 as +                # SSH sessions are not all yet properly cleaned away, thus we +                # simply re-run the command until the account wen't away +                while run(f'userdel --remove {user}', stderr=DEVNULL): +                    sleep(0.250)              except Exception as e:                  raise ConfigError(f'Deleting user "{user}" raised exception: {e}') diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 3d8a51cd8..309b4bdb0 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -17,6 +17,7 @@  import os  import re +from pathlib import Path  from sys import exit  from vyos.config import Config @@ -89,7 +90,7 @@ def get_config(config=None):                      filename: {                          'log-file': '/var/log/user/' + filename,                          'max-files': '5', -                        'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, +                        'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename,                          'selectors': '*.err',                          'max-size': 262144                      } @@ -205,10 +206,17 @@ def generate(c):      conf = '/etc/rsyslog.d/vyos-rsyslog.conf'      render(conf, 'syslog/rsyslog.conf.tmpl', c) +    # cleanup current logrotate config files +    logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*') +    for file in logrotate_files: +        file.unlink() +      # eventually write for each file its own logrotate file, since size is      # defined it shouldn't matter -    conf = '/etc/logrotate.d/vyos-rsyslog' -    render(conf, 'syslog/logrotate.tmpl', c) +    for filename, fileconfig in c.get('files', {}).items(): +        if fileconfig['log-file'].startswith('/var/log/user/'): +            conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename +            render(conf, 'syslog/logrotate.tmpl', { 'config_render': fileconfig })  def verify(c): diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 38c0c4463..f79c8a21e 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -29,6 +29,7 @@ from vyos.util import dict_search  from vyos.util import get_interface_config  from vyos.util import popen  from vyos.util import run +from vyos.util import sysctl_write  from vyos import ConfigError  from vyos import frr  from vyos import airbag @@ -37,10 +38,16 @@ airbag.enable()  config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'  nft_vrf_config = '/tmp/nftables-vrf-zones' -def list_rules(): -    command = 'ip -j -4 rule show' -    answer = loads(cmd(command)) -    return [_ for _ in answer if _] +def has_rule(af : str, priority : int, table : str): +    """ Check if a given ip rule exists """ +    if af not in ['-4', '-6']: +        raise ValueError() +    command = f'ip -j {af} rule show' +    for tmp in loads(cmd(command)): +        if {'priority', 'table'} <= set(tmp): +            if tmp['priority'] == priority and tmp['table'] == table: +                return True +    return False  def vrf_interfaces(c, match):      matched = [] @@ -69,7 +76,6 @@ def vrf_routing(c, match):      c.set_level(old_level)      return matched -  def get_config(config=None):      if config:          conf = config @@ -148,13 +154,11 @@ def apply(vrf):      bind_all = '0'      if 'bind-to-all' in vrf:          bind_all = '1' -    call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') -    call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') +    sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all) +    sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)      for tmp in (dict_search('vrf_remove', vrf) or []):          if os.path.isdir(f'/sys/class/net/{tmp}'): -            call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272') -            call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')              call(f'ip link delete dev {tmp}')              # Remove nftables conntrack zone map item              nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' @@ -165,31 +169,59 @@ def apply(vrf):          # check if table already exists          _, err = popen('nft list table inet vrf_zones')          # If not, create a table -        if err: -            if os.path.exists(nft_vrf_config): -                cmd(f'nft -f {nft_vrf_config}') -                os.unlink(nft_vrf_config) +        if err and os.path.exists(nft_vrf_config): +            cmd(f'nft -f {nft_vrf_config}') +            os.unlink(nft_vrf_config) + +        # Linux routing uses rules to find tables - routing targets are then +        # looked up in those tables. If the lookup got a matching route, the +        # process ends. +        # +        # TL;DR; first table with a matching entry wins! +        # +        # You can see your routing table lookup rules using "ip rule", sadly the +        # local lookup is hit before any VRF lookup. Pinging an addresses from the +        # VRF will usually find a hit in the local table, and never reach the VRF +        # routing table - this is usually not what you want. Thus we will +        # re-arrange the tables and move the local lookup further down once VRFs +        # are enabled. +        # +        # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html + +        for afi in ['-4', '-6']: +            # move lookup local to pref 32765 (from 0) +            if not has_rule(afi, 32765, 'local'): +                call(f'ip {afi} rule add pref 32765 table local') +            if has_rule(afi, 0, 'local'): +                call(f'ip {afi} rule del pref 0') +            # make sure that in VRFs after failed lookup in the VRF specific table +            # nothing else is reached +            if not has_rule(afi, 1000, 'l3mdev'): +                # this should be added by the kernel when a VRF is created +                # add it here for completeness +                call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel') + +            # add another rule with an unreachable target which only triggers in VRF context +            # if a route could not be reached +            if not has_rule(afi, 2000, 'l3mdev'): +                call(f'ip {afi} rule add pref 2000 l3mdev unreachable')          for name, config in vrf['name'].items():              table = config['table'] -              if not os.path.isdir(f'/sys/class/net/{name}'):                  # For each VRF apart from your default context create a VRF                  # interface with a separate routing table                  call(f'ip link add {name} type vrf table {table}') -                # The kernel Documentation/networking/vrf.txt also recommends -                # adding unreachable routes to the VRF routing tables so that routes -                # afterwards are taken. -                call(f'ip -4 route add vrf {name} unreachable default metric 4278198272') -                call(f'ip -6 route add vrf {name} unreachable default metric 4278198272') -                # We also should add proper loopback IP addresses to the newly -                # created VRFs for services bound to the loopback address (SNMP, NTP) -                call(f'ip -4 addr add 127.0.0.1/8 dev {name}') -                call(f'ip -6 addr add ::1/128 dev {name}')              # set VRF description for e.g. SNMP monitoring              vrf_if = Interface(name) +            # We also should add proper loopback IP addresses to the newly added +            # VRF for services bound to the loopback address (SNMP, NTP) +            vrf_if.add_addr('127.0.0.1/8') +            vrf_if.add_addr('::1/128') +            # add VRF description if available              vrf_if.set_alias(config.get('description', '')) +              # Enable/Disable of an interface must always be done at the end of the              # derived class to make use of the ref-counting set_admin_state()              # function. We will only enable the interface if 'up' was called as @@ -203,37 +235,9 @@ def apply(vrf):              nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'              cmd(f'nft {nft_add_element}') -    # Linux routing uses rules to find tables - routing targets are then -    # looked up in those tables. If the lookup got a matching route, the -    # process ends. -    # -    # TL;DR; first table with a matching entry wins! -    # -    # You can see your routing table lookup rules using "ip rule", sadly the -    # local lookup is hit before any VRF lookup. Pinging an addresses from the -    # VRF will usually find a hit in the local table, and never reach the VRF -    # routing table - this is usually not what you want. Thus we will -    # re-arrange the tables and move the local lookup furhter down once VRFs -    # are enabled. - -    # get current preference on local table -    local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] - -    # change preference when VRFs are enabled and local lookup table is default -    if not local_pref and 'name' in vrf: -        for af in ['-4', '-6']: -            call(f'ip {af} rule add pref 32765 table local') -            call(f'ip {af} rule del pref 0')      # return to default lookup preference when no VRF is configured      if 'name' not in vrf: -        for af in ['-4', '-6']: -            call(f'ip {af} rule add pref 0 table local') -            call(f'ip {af} rule del pref 32765') - -            # clean out l3mdev-table rule if present -            if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: -                call(f'ip {af} rule del pref 1000')          # Remove VRF zones table from nftables          tmp = run('nft list table inet vrf_zones')          if tmp == 0: diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index 683f8f034..dc0617353 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -20,10 +20,12 @@ from json import loads  from sys import exit  from vyos.config import Config +from vyos.configdict import dict_merge  from vyos.template import render  from vyos.util import cmd  from vyos.util import dict_search_args  from vyos.util import run +from vyos.xml import defaults  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -36,12 +38,22 @@ def get_config(config=None):      else:          conf = Config()      base = ['zone-policy'] -    zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, -                                    no_tag_node_value_mangle=True) +    zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), +                                       get_first_key=True, +                                       no_tag_node_value_mangle=True) -    if zone_policy: -        zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, -                                    no_tag_node_value_mangle=True) +    zone_policy['firewall'] = conf.get_config_dict(['firewall'], +                                                   key_mangling=('-', '_'), +                                                   get_first_key=True, +                                                   no_tag_node_value_mangle=True) + +    if 'zone' in zone_policy: +        # 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 + ['zone']) +        for zone in zone_policy['zone']: +            zone_policy['zone'][zone] = dict_merge(default_values, +                                                   zone_policy['zone'][zone])      return zone_policy diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan deleted file mode 100644 index 28190776f..000000000 --- a/src/etc/cron.d/check-wwan +++ /dev/null @@ -1 +0,0 @@ -*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py diff --git a/src/etc/logrotate.d/conntrackd b/src/etc/logrotate.d/conntrackd new file mode 100644 index 000000000..b0b09dec1 --- /dev/null +++ b/src/etc/logrotate.d/conntrackd @@ -0,0 +1,9 @@ +/var/log/conntrackd-stats.log { +    weekly +    rotate 2 +    missingok + +    postrotate +        systemctl restart conntrackd.service > /dev/null +    endscript +} diff --git a/src/etc/logrotate.d/vyos-rsyslog b/src/etc/logrotate.d/vyos-rsyslog new file mode 100644 index 000000000..3c087b94e --- /dev/null +++ b/src/etc/logrotate.d/vyos-rsyslog @@ -0,0 +1,12 @@ +/var/log/messages { +    create +    missingok +    nomail +    notifempty +    rotate 10 +    size 1M +    postrotate +        # inform rsyslog service about rotation +        /usr/lib/rsyslog/rsyslog-rotate +    endscript +} diff --git a/src/etc/systemd/system/uacctd.service.d/override.conf b/src/etc/systemd/system/uacctd.service.d/override.conf deleted file mode 100644 index 38bcce515..000000000 --- a/src/etc/systemd/system/uacctd.service.d/override.conf +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -After= -After=vyos-router.service -ConditionPathExists= -ConditionPathExists=/run/pmacct/uacctd.conf - -[Service] -EnvironmentFile= -ExecStart= -ExecStart=/usr/sbin/uacctd -f /run/pmacct/uacctd.conf -WorkingDirectory= -WorkingDirectory=/run/pmacct -PIDFile= -PIDFile=/run/pmacct/uacctd.pid diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py new file mode 100755 index 000000000..bf4bfd05d --- /dev/null +++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import json +import re +import time + +from vyos.util import cmd + + +def get_nft_filter_chains(): +    """ +    Get list of nft chains for table filter +    """ +    nft = cmd('/usr/sbin/nft --json list table ip filter') +    nft = json.loads(nft) +    chain_list = [] + +    for output in nft['nftables']: +        if 'chain' in output: +            chain = output['chain']['name'] +            chain_list.append(chain) + +    return chain_list + + +def get_nftables_details(name): +    """ +    Get dict, counters packets and bytes for chain +    """ +    command = f'/usr/sbin/nft list chain ip filter {name}' +    try: +        results = cmd(command) +    except: +        return {} + +    # Trick to remove 'NAME_' from chain name in the comment +    # It was added to any chain T4218 +    # counter packets 0 bytes 0 return comment "FOO default-action accept" +    comment_name = name.replace("NAME_", "") +    out = {} +    for line in results.split('\n'): +        comment_search = re.search(rf'{comment_name}[\- ](\d+|default-action)', line) +        if not comment_search: +            continue + +        rule = {} +        rule_id = comment_search[1] +        counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line) +        if counter_search: +            rule['packets'] = counter_search[1] +            rule['bytes'] = counter_search[2] + +        rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip() +        out[rule_id] = rule +    return out + + +def get_nft_telegraf(name): +    """ +    Get data for telegraf in influxDB format +    """ +    for rule, rule_config in get_nftables_details(name).items(): +        print(f'nftables,table=filter,chain={name},' +              f'ruleid={rule} ' +              f'pkts={rule_config["packets"]}i,' +              f'bytes={rule_config["bytes"]}i ' +              f'{str(int(time.time()))}000000000') + + +chains = get_nft_filter_chains() + +for chain in chains: +    get_nft_telegraf(chain) 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 0f5e366cd..0c7474156 100755 --- a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py +++ b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py @@ -1,47 +1,88 @@  #!/usr/bin/env python3 -import subprocess +from vyos.ifconfig import Section +from vyos.ifconfig import Interface +  import time -def status_to_int(status): -    switcher={ -        'u':'0', -        'D':'1', -        'A':'2' -        } -    return switcher.get(status,"") - -def description_check(line): -    desc=" ".join(line[3:]) -    if desc == "": +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 +    By default don't get IPv6 link-local addresses +    If interface doesn't have address, return "-" +    """ +    addresses = [] +    addrs = Interface(iface).get_addr() + +    for addr in addrs: +        if link_local_v6 == False: +            if addr.startswith('fe80::'): +                continue +        addresses.append(addr) + +    if not addresses: +        return "-" + +    return (" ".join(addresses)) + +def get_interface_description(iface): +    """ +    Get interface description +    If none return "empty" +    """ +    description = Interface(iface).get_alias() + +    if not description:          return "empty" + +    return description + +def get_interface_admin_state(iface): +    """ +    Interface administrative state +    up => 0, down => 2 +    """ +    state = Interface(iface).get_admin_state() +    if state == 'up': +        admin_state = 0 +    if state == 'down': +        admin_state = 2 + +    return admin_state + +def get_interface_oper_state(iface): +    """ +    Interface operational state +    up => 0, down => 1 +    """ +    state = Interface(iface).operational.get_state() +    if state == 'down': +        oper_state = 1      else: -        return desc - -def gen_ip_list(index,interfaces): -    line=interfaces[index].split() -    ip_list=line[1] -    if index < len(interfaces): -        index += 1 -        while len(interfaces[index].split())==1: -            ip = interfaces[index].split() -            ip_list = ip_list + " " + ip[0] -            index += 1 -            if index == len(interfaces): -                break -    return ip_list - -interfaces = subprocess.check_output("/usr/libexec/vyos/op_mode/show_interfaces.py --action=show-brief", shell=True).decode('utf-8').splitlines() -del interfaces[:3] -lines_count=len(interfaces) -index=0 -while index<lines_count: -    line=interfaces[index].split() -    if len(line)>1: -        print(f'show_interfaces,interface={line[0]} ' -              f'ip_addresses="{gen_ip_list(index,interfaces)}",' -              f'state={status_to_int(line[2][0])}i,' -              f'link={status_to_int(line[2][2])}i,' -              f'description="{description_check(line)}" ' -              f'{str(int(time.time()))}000000000') -    index += 1 +        oper_state = 0 + +    return oper_state + +interfaces = get_interfaces() + +for iface in interfaces: +    print(f'show_interfaces,interface={iface} ' +          f'ip_addresses="{get_interface_addresses(iface)}",' +          f'state={get_interface_admin_state(iface)}i,' +          f'link={get_interface_oper_state(iface)}i,' +          f'description="{get_interface_description(iface)}" ' +          f'{str(int(time.time()))}000000000') diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py index e4e1fe11d..eb584edaf 100755 --- a/src/helpers/strip-private.py +++ b/src/helpers/strip-private.py @@ -1,6 +1,6 @@  #!/usr/bin/python3 -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io>  #  # This library is free software; you can redistribute it and/or  # modify it under the terms of the GNU Lesser General Public @@ -111,6 +111,10 @@ if __name__ == "__main__":          (True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'),          (True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'),          (True, re.compile(r' key \S+'), ' key xxxxxx'), +        # Strip bucket +        (True, re.compile(r' bucket \S+'), ' bucket xxxxxx'), +        # Strip tokens +        (True, re.compile(r' token \S+'), ' token xxxxxx'),          # Strip OpenVPN secrets          (True, re.compile(r'(shared-secret-key-file|ca-cert-file|cert-file|dh-file|key-file|client) (\S+)'), r'\1 xxxxxx'),          # Strip IPSEC secrets @@ -123,8 +127,8 @@ if __name__ == "__main__":          # Strip MAC addresses          (args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'xx:xx:xx:xx:xx:\2'), -        # Strip host-name, domain-name, and domain-search -        (args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'), +        # Strip host-name, domain-name, domain-search and url +        (args.hostname, re.compile(r'(host-name|domain-name|domain-search|url) \S+'), r'\1 xxxxxx'),          # Strip user-names          (args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'), diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py index c33e41d79..2aa687221 100755 --- a/src/helpers/system-versions-foot.py +++ b/src/helpers/system-versions-foot.py @@ -21,7 +21,7 @@ import vyos.systemversions as systemversions  import vyos.defaults  import vyos.version -sys_versions = systemversions.get_system_versions() +sys_versions = systemversions.get_system_component_version()  component_string = formatversions.format_versions_string(sys_versions) diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh index 4501aa63e..0cc718938 100755 --- a/src/helpers/vyos-vrrp-conntracksync.sh +++ b/src/helpers/vyos-vrrp-conntracksync.sh @@ -14,12 +14,10 @@  # Modified by : Mohit Mehta <mohit@vyatta.com>  # Slight modifications were made to this script for running with Vyatta  # The original script came from 0.9.14 debian conntrack-tools package -# -#  CONNTRACKD_BIN=/usr/sbin/conntrackd  CONNTRACKD_LOCK=/var/lock/conntrack.lock -CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf +CONNTRACKD_CONFIG=/run/conntrackd/conntrackd.conf  FACILITY=daemon  LEVEL=notice  TAG=conntrack-tools @@ -29,6 +27,10 @@ FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state"  $LOGCMD "vyatta-vrrp-conntracksync invoked at `date`" +if ! systemctl is-active --quiet conntrackd.service; then +    echo "conntrackd service not running" +    exit 1 +fi  if [ ! -e $FAILOVER_STATE ]; then  	mkdir -p /var/run diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name index afeef8f2d..1798e92db 100755 --- a/src/helpers/vyos_net_name +++ b/src/helpers/vyos_net_name @@ -20,12 +20,14 @@ import os  import re  import time  import logging +import tempfile  import threading  from sys import argv  from vyos.configtree import ConfigTree  from vyos.defaults import directories  from vyos.util import cmd, boot_configuration_complete +from vyos.migrator import VirtualMigrator  vyos_udev_dir = directories['vyos_udev_dir']  vyos_log_dir = '/run/udev/log' @@ -139,14 +141,20 @@ def get_configfile_interfaces() -> dict:      try:          config = ConfigTree(config_file)      except Exception: -        logging.debug(f"updating component version string syntax")          try: -            # this will update the component version string in place, for -            # updates 1.2 --> 1.3/1.4 -            os.system(f'/usr/libexec/vyos/run-config-migration.py {config_path} --virtual --set-vintage=vyos') -            with open(config_path) as f: -                config_file = f.read() +            logging.debug(f"updating component version string syntax") +            # this will update the component version string syntax, +            # required for updates 1.2 --> 1.3/1.4 +            with tempfile.NamedTemporaryFile() as fp: +                with open(fp.name, 'w') as fd: +                    fd.write(config_file) +                virtual_migration = VirtualMigrator(fp.name) +                virtual_migration.run() +                with open(fp.name) as fd: +                    config_file = fd.read() +              config = ConfigTree(config_file) +          except Exception as e:              logging.critical(f"ConfigTree error: {e}") @@ -246,4 +254,3 @@ if not boot_configuration_complete():  else:      logging.debug("boot configuration complete")  lock.release() - diff --git a/src/migration-scripts/bgp/0-to-1 b/src/migration-scripts/bgp/0-to-1 index b1d5a6514..5e9dffe1f 100755 --- a/src/migration-scripts/bgp/0-to-1 +++ b/src/migration-scripts/bgp/0-to-1 @@ -33,7 +33,7 @@ with open(file_name, 'r') as f:  base = ['protocols', 'bgp']  config = ConfigTree(config_file) -if not config.exists(base): +if not config.exists(base) or not config.is_tag(base):      # Nothing to do      exit(0) diff --git a/src/migration-scripts/bgp/1-to-2 b/src/migration-scripts/bgp/1-to-2 index 4c6d5ceb8..e2d3fcd33 100755 --- a/src/migration-scripts/bgp/1-to-2 +++ b/src/migration-scripts/bgp/1-to-2 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -20,7 +20,6 @@ from sys import argv  from sys import exit  from vyos.configtree import ConfigTree -from vyos.template import is_ipv4  if (len(argv) < 1):      print("Must specify file name!") @@ -51,23 +50,21 @@ if config.exists(base + ['parameters', 'default', 'no-ipv4-unicast']):      # Check if the "default" node is now empty, if so - remove it      if len(config.list_nodes(base + ['parameters'])) == 0:          config.delete(base + ['parameters']) +else: +    # As we now install a new default option into BGP we need to migrate all +    # existing BGP neighbors and restore the old behavior +    if config.exists(base + ['neighbor']): +        for neighbor in config.list_nodes(base + ['neighbor']): +            peer_group = base + ['neighbor', neighbor, 'peer-group'] +            if config.exists(peer_group): +                peer_group_name = config.return_value(peer_group) +                # peer group enables old behavior for neighbor - bail out +                if config.exists(base + ['peer-group', peer_group_name, 'address-family', 'ipv4-unicast']): +                    continue -    exit(0) - -# As we now install a new default option into BGP we need to migrate all -# existing BGP neighbors and restore the old behavior -if config.exists(base + ['neighbor']): -    for neighbor in config.list_nodes(base + ['neighbor']): -        peer_group = base + ['neighbor', neighbor, 'peer-group'] -        if config.exists(peer_group): -            peer_group_name = config.return_value(peer_group) -            # peer group enables old behavior for neighbor - bail out -            if config.exists(base + ['peer-group', peer_group_name, 'address-family', 'ipv4-unicast']): -                continue - -        afi_ipv4 = base + ['neighbor', neighbor, 'address-family', 'ipv4-unicast'] -        if not config.exists(afi_ipv4): -            config.set(afi_ipv4) +            afi_ipv4 = base + ['neighbor', neighbor, 'address-family', 'ipv4-unicast'] +            if not config.exists(afi_ipv4): +                config.set(afi_ipv4)  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index ba10c26f2..a8c930be7 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -16,7 +16,7 @@  #  # This migration script will remove the deprecated 'listen-on' statement -# from the dns forwarding service and will add the corresponding  +# from the dns forwarding service and will add the corresponding  # listen-address nodes instead. This is required as PowerDNS can only listen  # on interface addresses and not on interface names. @@ -37,53 +37,50 @@ with open(file_name, 'r') as f:  config = ConfigTree(config_file)  base = ['service', 'dns', 'forwarding'] -if not config.exists(base): +if not config.exists(base + ['listen-on']):      # Nothing to do      exit(0) -if config.exists(base + ['listen-on']): -    listen_intf = config.return_values(base + ['listen-on']) -    # Delete node with abandoned command -    config.delete(base + ['listen-on']) +listen_intf = config.return_values(base + ['listen-on']) +# Delete node with abandoned command +config.delete(base + ['listen-on']) -    # retrieve interface addresses for every configured listen-on interface -    listen_addr = [] -    for intf in listen_intf: -        # we need to evaluate the interface section before manipulating the 'intf' variable -        section = Interface.section(intf) -        if not section: -            raise ValueError(f'Invalid interface name {intf}') +# retrieve interface addresses for every configured listen-on interface +listen_addr = [] +for intf in listen_intf: +    # we need to evaluate the interface section before manipulating the 'intf' variable +    section = Interface.section(intf) +    if not section: +        raise ValueError(f'Invalid interface name {intf}') -        # we need to treat vif and vif-s interfaces differently, -        # both "real interfaces" use dots for vlan identifiers - those -        # need to be exchanged with vif and vif-s identifiers -        if intf.count('.') == 1: -            # this is a regular VLAN interface -            intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] -        elif intf.count('.') == 2: -            # this is a QinQ VLAN interface -            intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' +  intf.split('.')[2] - -        # retrieve corresponding interface addresses in CIDR format -        # those need to be converted in pure IP addresses without network information -        path = ['interfaces', section, intf, 'address'] -        try: -            for addr in config.return_values(path): -                listen_addr.append( ip_interface(addr).ip ) -        except: -            # Some interface types do not use "address" option (e.g. OpenVPN) -            # and may not even have a fixed address -            print("Could not retrieve the address of the interface {} from the config".format(intf)) -            print("You will need to update your DNS forwarding configuration manually") - -    for addr in listen_addr: -        config.set(base + ['listen-address'], value=addr, replace=False) +    # we need to treat vif and vif-s interfaces differently, +    # both "real interfaces" use dots for vlan identifiers - those +    # need to be exchanged with vif and vif-s identifiers +    if intf.count('.') == 1: +        # this is a regular VLAN interface +        intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] +    elif intf.count('.') == 2: +        # this is a QinQ VLAN interface +        intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' +  intf.split('.')[2] +    # retrieve corresponding interface addresses in CIDR format +    # those need to be converted in pure IP addresses without network information +    path = ['interfaces', section, intf, 'address']      try: -        with open(file_name, 'w') as f: -            f.write(config.to_string()) -    except OSError as e: -        print("Failed to save the modified config: {}".format(e)) -        exit(1) +        for addr in config.return_values(path): +            listen_addr.append( ip_interface(addr).ip ) +    except: +        # Some interface types do not use "address" option (e.g. OpenVPN) +        # and may not even have a fixed address +        print("Could not retrieve the address of the interface {} from the config".format(intf)) +        print("You will need to update your DNS forwarding configuration manually") -exit(0) +for addr in listen_addr: +    config.set(base + ['listen-address'], value=addr, replace=False) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 index 4a4097d56..5f4cff90d 100755 --- a/src/migration-scripts/firewall/6-to-7 +++ b/src/migration-scripts/firewall/6-to-7 @@ -17,6 +17,10 @@  # T2199: Remove unavailable nodes due to XML/Python implementation using nftables  #        monthdays: nftables does not have a monthdays equivalent  #        utc: nftables userspace uses localtime and calculates the UTC offset automatically +#        icmp/v6: migrate previously available `type-name` to valid type/code +# T4178: Update tcp flags to use multi value node + +import re  from sys import argv  from sys import exit @@ -40,29 +44,179 @@ if not config.exists(base):      # Nothing to do      exit(0) +icmp_remove = ['any'] +icmp_translations = { +    'ping': 'echo-request', +    'pong': 'echo-reply', +    'ttl-exceeded': 'time-exceeded', +    # Network Unreachable +    'network-unreachable': [3, 0], +    'host-unreachable': [3, 1], +    'protocol-unreachable': [3, 2], +    'port-unreachable': [3, 3], +    'fragmentation-needed': [3, 4], +    'source-route-failed': [3, 5], +    'network-unknown': [3, 6], +    'host-unknown': [3, 7], +    'network-prohibited': [3, 9], +    'host-prohibited': [3, 10], +    'TOS-network-unreachable': [3, 11], +    'TOS-host-unreachable': [3, 12], +    'communication-prohibited': [3, 13], +    'host-precedence-violation': [3, 14], +    'precedence-cutoff': [3, 15], +    # Redirect +    'network-redirect': [5, 0], +    'host-redirect': [5, 1], +    'TOS-network-redirect': [5, 2], +    'TOS host-redirect': [5, 3], +    #  Time Exceeded +    'ttl-zero-during-transit': [11, 0], +    'ttl-zero-during-reassembly': [11, 1], +    # Parameter Problem +    'ip-header-bad': [12, 0], +    'required-option-missing': [12, 1] +} + +icmpv6_remove = [] +icmpv6_translations = { +    'ping': 'echo-request', +    'pong': 'echo-reply', +    # Destination Unreachable +    'no-route': [1, 0], +    'communication-prohibited': [1, 1], +    'address-unreachble': [1, 3], +    'port-unreachable': [1, 4], +    # Redirect +    'redirect': 'nd-redirect', +    #  Time Exceeded +    'ttl-zero-during-transit': [3, 0], +    'ttl-zero-during-reassembly': [3, 1], +    # Parameter Problem +    'bad-header': [4, 0], +    'unknown-header-type': [4, 1], +    'unknown-option': [4, 2] +} +  if config.exists(base + ['name']):      for name in config.list_nodes(base + ['name']): -        if config.exists(base + ['name', name, 'rule']): -            for rule in config.list_nodes(base + ['name', name, 'rule']): -                rule_time = base + ['name', name, 'rule', rule, 'time'] +        if not config.exists(base + ['name', name, 'rule']): +            continue + +        for rule in config.list_nodes(base + ['name', name, 'rule']): +            rule_recent = base + ['name', name, 'rule', rule, 'recent'] +            rule_time = base + ['name', name, 'rule', rule, 'time'] +            rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags'] +            rule_icmp = base + ['name', name, 'rule', rule, 'icmp'] + +            if config.exists(rule_time + ['monthdays']): +                config.delete(rule_time + ['monthdays']) + +            if config.exists(rule_time + ['utc']): +                config.delete(rule_time + ['utc']) + +            if config.exists(rule_recent + ['time']): +                tmp = int(config.return_value(rule_recent + ['time'])) +                unit = 'minute' +                if tmp > 600: +                    unit = 'hour' +                elif tmp < 10: +                    unit = 'second' +                config.set(rule_recent + ['time'], value=unit) + +            if config.exists(rule_tcp_flags): +                tmp = config.return_value(rule_tcp_flags) +                config.delete(rule_tcp_flags) +                for flag in tmp.split(","): +                    if flag[0] == '!': +                        config.set(rule_tcp_flags + ['not', flag[1:].lower()]) +                    else: +                        config.set(rule_tcp_flags + [flag.lower()]) -                if config.exists(rule_time + ['monthdays']): -                    config.delete(rule_time + ['monthdays']) +            if config.exists(rule_icmp + ['type-name']): +                tmp = config.return_value(rule_icmp + ['type-name']) +                if tmp in icmp_remove: +                    config.delete(rule_icmp + ['type-name']) +                elif tmp in icmp_translations: +                    translate = icmp_translations[tmp] +                    if isinstance(translate, str): +                        config.set(rule_icmp + ['type-name'], value=translate) +                    elif isinstance(translate, list): +                        config.delete(rule_icmp + ['type-name']) +                        config.set(rule_icmp + ['type'], value=translate[0]) +                        config.set(rule_icmp + ['code'], value=translate[1]) -                if config.exists(rule_time + ['utc']): -                    config.delete(rule_time + ['utc']) +            for src_dst in ['destination', 'source']: +                pg_base = base + ['name', name, 'rule', rule, src_dst, 'group', 'port-group'] +                proto_base = base + ['name', name, 'rule', rule, 'protocol'] +                if config.exists(pg_base) and not config.exists(proto_base): +                    config.set(proto_base, value='tcp_udp')  if config.exists(base + ['ipv6-name']):      for name in config.list_nodes(base + ['ipv6-name']): -        if config.exists(base + ['ipv6-name', name, 'rule']): -            for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): -                rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] +        if not config.exists(base + ['ipv6-name', name, 'rule']): +            continue + +        for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): +            rule_recent = base + ['ipv6-name', name, 'rule', rule, 'recent'] +            rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] +            rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags'] +            rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6'] + +            if config.exists(rule_time + ['monthdays']): +                config.delete(rule_time + ['monthdays']) + +            if config.exists(rule_time + ['utc']): +                config.delete(rule_time + ['utc']) + +            if config.exists(rule_recent + ['time']): +                tmp = int(config.return_value(rule_recent + ['time'])) +                unit = 'minute' +                if tmp > 600: +                    unit = 'hour' +                elif tmp < 10: +                    unit = 'second' +                config.set(rule_recent + ['time'], value=unit) + +            if config.exists(rule_tcp_flags): +                tmp = config.return_value(rule_tcp_flags) +                config.delete(rule_tcp_flags) +                for flag in tmp.split(","): +                    if flag[0] == '!': +                        config.set(rule_tcp_flags + ['not', flag[1:].lower()]) +                    else: +                        config.set(rule_tcp_flags + [flag.lower()]) + +            if config.exists(base + ['ipv6-name', name, 'rule', rule, 'protocol']): +                tmp = config.return_value(base + ['ipv6-name', name, 'rule', rule, 'protocol']) +                if tmp == 'icmpv6': +                    config.set(base + ['ipv6-name', name, 'rule', rule, 'protocol'], value='ipv6-icmp') + +            if config.exists(rule_icmp + ['type']): +                tmp = config.return_value(rule_icmp + ['type']) +                type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) -                if config.exists(rule_time + ['monthdays']): -                    config.delete(rule_time + ['monthdays']) +                if type_code_match: +                    config.set(rule_icmp + ['type'], value=type_code_match[1]) +                    config.set(rule_icmp + ['code'], value=type_code_match[2]) +                elif tmp in icmpv6_remove: +                    config.delete(rule_icmp + ['type']) +                elif tmp in icmpv6_translations: +                    translate = icmpv6_translations[tmp] +                    if isinstance(translate, str): +                        config.delete(rule_icmp + ['type']) +                        config.set(rule_icmp + ['type-name'], value=translate) +                    elif isinstance(translate, list): +                        config.set(rule_icmp + ['type'], value=translate[0]) +                        config.set(rule_icmp + ['code'], value=translate[1]) +                else: +                    config.rename(rule_icmp + ['type'], 'type-name') -                if config.exists(rule_time + ['utc']): -                    config.delete(rule_time + ['utc']) +            for src_dst in ['destination', 'source']: +                pg_base = base + ['ipv6-name', name, 'rule', rule, src_dst, 'group', 'port-group'] +                proto_base = base + ['ipv6-name', name, 'rule', rule, 'protocol'] +                if config.exists(pg_base) and not config.exists(proto_base): +                    config.set(proto_base, value='tcp_udp')  try:      with open(file_name, 'w') as f: diff --git a/src/migration-scripts/ipsec/8-to-9 b/src/migration-scripts/ipsec/8-to-9 new file mode 100755 index 000000000..eb44b6216 --- /dev/null +++ b/src/migration-scripts/ipsec/8-to-9 @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['vpn', 'ipsec', 'ike-group'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) +else: +    for ike_group in config.list_nodes(base): +        base_closeaction = base + [ike_group, 'close-action'] +        if config.exists(base_closeaction) and config.return_value(base_closeaction) == 'clear': +            config.set(base_closeaction, 'none', replace=True) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/policy/1-to-2 b/src/migration-scripts/policy/1-to-2 new file mode 100755 index 000000000..eebbf9d41 --- /dev/null +++ b/src/migration-scripts/policy/1-to-2 @@ -0,0 +1,86 @@ +#!/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/>. + +# T4170: rename "policy ipv6-route" to "policy route6" to match common +#        IPv4/IPv6 schema +# T4178: Update tcp flags to use multi value node + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['policy', 'ipv6-route'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +config.rename(base, 'route6') +config.set_tag(['policy', 'route6']) + +for route in ['route', 'route6']: +    route_path = ['policy', route] +    if config.exists(route_path): +        for name in config.list_nodes(route_path): +            if config.exists(route_path + [name, 'rule']): +                for rule in config.list_nodes(route_path + [name, 'rule']): +                    rule_tcp_flags = route_path + [name, 'rule', rule, 'tcp', 'flags'] + +                    if config.exists(rule_tcp_flags): +                        tmp = config.return_value(rule_tcp_flags) +                        config.delete(rule_tcp_flags) +                        for flag in tmp.split(","): +                            for flag in tmp.split(","): +                                if flag[0] == '!': +                                    config.set(rule_tcp_flags + ['not', flag[1:].lower()]) +                                else: +                                    config.set(rule_tcp_flags + [flag.lower()]) + +if config.exists(['interfaces']): +    def if_policy_rename(config, path): +        if config.exists(path + ['policy', 'ipv6-route']): +            config.rename(path + ['policy', 'ipv6-route'], 'route6') + +    for if_type in config.list_nodes(['interfaces']): +        for ifname in config.list_nodes(['interfaces', if_type]): +            if_path = ['interfaces', if_type, ifname] +            if_policy_rename(config, if_path) + +        for vif_type in ['vif', 'vif-s']: +            if config.exists(if_path + [vif_type]): +                for vifname in config.list_nodes(if_path + [vif_type]): +                    if_policy_rename(config, if_path + [vif_type, vifname]) + +                    if config.exists(if_path + [vif_type, vifname, 'vif-c']): +                        for vifcname in config.list_nodes(if_path + [vif_type, vifname, 'vif-c']): +                            if_policy_rename(config, if_path + [vif_type, vifname, 'vif-c', vifcname]) +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2 index bc8815753..31c40df16 100755 --- a/src/migration-scripts/ssh/1-to-2 +++ b/src/migration-scripts/ssh/1-to-2 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -30,26 +30,52 @@ file_name = argv[1]  with open(file_name, 'r') as f:      config_file = f.read() -base = ['service', 'ssh', 'loglevel'] +base = ['service', 'ssh']  config = ConfigTree(config_file)  if not config.exists(base):      # Nothing to do      exit(0) -else: -    # red in configured loglevel and convert it to lower case -    tmp = config.return_value(base).lower() +path_loglevel = base + ['loglevel'] +if config.exists(path_loglevel): +    # red in configured loglevel and convert it to lower case +    tmp = config.return_value(path_loglevel).lower()      # VyOS 1.2 had no proper value validation on the CLI thus the      # user could use any arbitrary values - sanitize them      if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']:          tmp = 'info' +    config.set(path_loglevel, value=tmp) + +# T4273: migrate ssh cipher list to multi node +path_ciphers = base + ['ciphers'] +if config.exists(path_ciphers): +    tmp = [] +    # get curtrent cipher list - comma delimited +    for cipher in config.return_values(path_ciphers): +        tmp.extend(cipher.split(',')) +    # delete old cipher suite representation +    config.delete(path_ciphers) -    config.set(base, value=tmp) +    for cipher in tmp: +        config.set(path_ciphers, value=cipher, replace=False) -    try: -        with open(file_name, 'w') as f: -            f.write(config.to_string()) -    except OSError as e: -        print("Failed to save the modified config: {}".format(e)) -        exit(1) +# T4273: migrate ssh key-exchange list to multi node +path_kex = base + ['key-exchange'] +if config.exists(path_kex): +    tmp = [] +    # get curtrent cipher list - comma delimited +    for kex in config.return_values(path_kex): +        tmp.extend(kex.split(',')) +    # delete old cipher suite representation +    config.delete(path_kex) + +    for kex in tmp: +        config.set(path_kex, value=kex, replace=False) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/system/22-to-23 b/src/migration-scripts/system/22-to-23 new file mode 100755 index 000000000..7f832e48a --- /dev/null +++ b/src/migration-scripts/system/22-to-23 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['system', 'ipv6'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +# T4346: drop support to disbale IPv6 address family within the OS Kernel +if config.exists(base + ['disable']): +    config.delete(base + ['disable']) +    # IPv6 address family disable was the only CLI option set - we can cleanup +    # the entire tree +    if len(config.list_nodes(base)) == 0: +        config.delete(base) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py index cfd321522..3bdf5a718 100755 --- a/src/op_mode/cpu_summary.py +++ b/src/op_mode/cpu_summary.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -19,18 +19,30 @@ from vyos.util import colon_separated_to_dict  FILE_NAME = '/proc/cpuinfo' -with open(FILE_NAME, 'r') as f: -    data_raw = f.read() +def get_raw_data(): +    with open(FILE_NAME, 'r') as f: +        data_raw = f.read() -data = colon_separated_to_dict(data_raw) +    data = colon_separated_to_dict(data_raw) -# Accumulate all data in a dict for future support for machine-readable output -cpu_data = {} -cpu_data['cpu_number'] = len(data['processor']) -cpu_data['models'] = list(set(data['model name'])) +    # Accumulate all data in a dict for future support for machine-readable output +    cpu_data = {} +    cpu_data['cpu_number'] = len(data['processor']) +    cpu_data['models'] = list(set(data['model name'])) -# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that -cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']) +    # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that +    cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])) + +    return cpu_data + +def get_formatted_output(): +    cpu_data = get_raw_data() + +    out = "CPU(s): {0}\n".format(cpu_data['cpu_number']) +    out += "CPU model(s): {0}".format(",".join(cpu_data['models'])) + +    return out + +if __name__ == '__main__': +    print(get_formatted_output()) -print("CPU(s): {0}".format(cpu_data['cpu_number'])) -print("CPU model(s): {0}".format(",".join(cpu_data['models']))) diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index cf70890a6..3146fc357 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -15,6 +15,7 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import argparse +import ipaddress  import json  import re  import tabulate @@ -87,7 +88,8 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):  def get_nftables_details(name, ipv6=False):      suffix = '6' if ipv6 else '' -    command = f'sudo nft list chain ip{suffix} filter {name}' +    name_prefix = 'NAME6_' if ipv6 else 'NAME_' +    command = f'sudo nft list chain ip{suffix} filter {name_prefix}{name}'      try:          results = cmd(command)      except: @@ -266,13 +268,17 @@ def show_firewall_group(name=None):                  continue              references = find_references(group_type, group_name) -            row = [group_name, group_type, ', '.join(references)] +            row = [group_name, group_type, '\n'.join(references) or 'N/A']              if 'address' in group_conf: -                row.append(", ".join(group_conf['address'])) +                row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address)))              elif 'network' in group_conf: -                row.append(", ".join(group_conf['network'])) +                row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) +            elif 'mac_address' in group_conf: +                row.append("\n".join(sorted(group_conf['mac_address'])))              elif 'port' in group_conf: -                row.append(", ".join(group_conf['port'])) +                row.append("\n".join(sorted(group_conf['port']))) +            else: +                row.append('N/A')              rows.append(row)      if rows: @@ -302,7 +308,7 @@ def show_summary():          for name, name_conf in firewall['ipv6_name'].items():              description = name_conf.get('description', '')              interfaces = ", ".join(name_conf['interface']) -            v6_out.append([name, description, interfaces]) +            v6_out.append([name, description, interfaces or 'N/A'])      if v6_out:          print('\nIPv6 name:\n') diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py new file mode 100755 index 000000000..29db41e37 --- /dev/null +++ b/src/op_mode/generate_ovpn_client_file.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os + +from jinja2 import Template + +from vyos.configquery import ConfigTreeQuery +from vyos.ifconfig import Section +from vyos.util import cmd + + +client_config = """ + +client +nobind +remote {{ remote_host }} {{ port }} +remote-cert-tls server +proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }} +dev {{ device }} +dev-type {{ device }} +persist-key +persist-tun +verb 3 + +# Encryption options +{% if encryption is defined and encryption is not none %} +{%   if encryption.cipher is defined and encryption.cipher is not none %} +cipher {{ encryption.cipher }} +{%     if encryption.cipher == 'bf128' %} +keysize 128 +{%     elif encryption.cipher == 'bf256' %} +keysize 256 +{%     endif %} +{%   endif %} +{%   if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %} +data-ciphers {{ encryption.ncp_ciphers }} +{%   endif %} +{% endif %} + +{% if hash is defined and hash is not none %} +auth {{ hash }} +{% endif %} +keysize 256 +comp-lzo {{ '' if use_lzo_compression is defined else 'no' }} + +<ca> +-----BEGIN CERTIFICATE----- +{{ ca }} +-----END CERTIFICATE----- + +</ca> + +<cert> +-----BEGIN CERTIFICATE----- +{{ cert }} +-----END CERTIFICATE----- + +</cert> + +<key> +-----BEGIN PRIVATE KEY----- +{{ key }} +-----END PRIVATE KEY----- + +</key> + +""" + +config = ConfigTreeQuery() +base = ['interfaces', 'openvpn'] + +if not config.exists(base): +    print('OpenVPN not configured') +    exit(0) + + +if __name__ == '__main__': +    parser = argparse.ArgumentParser() +    parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True) +    parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True) +    parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True) +    parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store") +    args = parser.parse_args() + +    interface = args.interface +    ca = args.ca +    cert = args.cert +    key = args.key +    if not key: +        key = args.cert + +    if interface not in Section.interfaces('openvpn'): +        exit(f'OpenVPN interface "{interface}" does not exist!') + +    if not config.exists(['pki', 'ca', ca, 'certificate']): +        exit(f'OpenVPN CA certificate "{ca}" does not exist!') + +    if not config.exists(['pki', 'certificate', cert, 'certificate']): +        exit(f'OpenVPN certificate "{cert}" does not exist!') + +    if not config.exists(['pki', 'certificate', cert, 'private', 'key']): +        exit(f'OpenVPN certificate key "{key}" does not exist!') + +    ca = config.value(['pki', 'ca', ca, 'certificate']) +    cert = config.value(['pki', 'certificate', cert, 'certificate']) +    key = config.value(['pki', 'certificate', key, 'private', 'key']) +    remote_host = config.value(base + [interface, 'local-host']) + +    ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True) + +    port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port'] +    proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol'] +    device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type'] + +    config = { +        'interface'   : interface, +        'ca'          : ca, +        'cert'        : cert, +        'key'         : key, +        'device'      : device, +        'port'        : port, +        'proto'       : proto, +        'remote_host' : remote_host, +        'address'     : [], +    } + +# Clear out terminal first +print('\x1b[2J\x1b[H') +client = Template(client_config, trim_blocks=True).render(config) +print(client) diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py index 7a7b6c923..f071ae350 100755 --- a/src/op_mode/generate_public_key_command.py +++ b/src/op_mode/generate_public_key_command.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -29,8 +29,12 @@ def get_key(path):          key_string = vyos.remote.get_remote_config(path)      return key_string.split() -username = sys.argv[1] -algorithm, key, identifier = get_key(sys.argv[2]) +try: +    username = sys.argv[1] +    algorithm, key, identifier = get_key(sys.argv[2]) +except Exception as e: +    print("Failed to retrieve the public key: {}".format(e)) +    sys.exit(1)  print('# To add this key as an embedded key, run the following commands:')  print('configure') @@ -39,3 +43,4 @@ print(f'set system login user {username} authentication public-keys {identifier}  print('commit')  print('save')  print('exit') + diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index b9ebc991a..17f6bf552 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -54,6 +54,7 @@ def parse_data(data, interface):          for local_if, values in neighbor.items():              if interface is not None and local_if != interface:                  continue +            cap = ''              for chassis, c_value in values.get('chassis', {}).items():                  # bail out early if no capabilities found                  if 'capability' not in c_value: @@ -62,7 +63,6 @@ def parse_data(data, interface):                  if isinstance(capabilities, dict):                      capabilities = [capabilities] -                cap = ''                  for capability in capabilities:                      if capability['enabled']:                          if capability['type'] == 'Router': diff --git a/src/op_mode/monitor_bandwidth_test.sh b/src/op_mode/monitor_bandwidth_test.sh index 900223bca..a6ad0b42c 100755 --- a/src/op_mode/monitor_bandwidth_test.sh +++ b/src/op_mode/monitor_bandwidth_test.sh @@ -24,6 +24,9 @@ elif [[ $(dig $1 AAAA +short | grep -v '\.$' | wc -l) -gt 0 ]]; then      # Set address family to IPv6 when FQDN has at least one AAAA record      OPT="-V" +else +    # It's not IPv6, no option needed +    OPT=""  fi  /usr/bin/iperf $OPT -c $1 $2 diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py index e0b4ac514..5be40082f 100755 --- a/src/op_mode/policy_route.py +++ b/src/op_mode/policy_route.py @@ -26,7 +26,7 @@ def get_policy_interfaces(conf, policy, name=None, ipv6=False):      interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),                                        get_first_key=True, no_tag_node_value_mangle=True) -    routes = ['route', 'ipv6_route'] +    routes = ['route', 'route6']      def parse_if(ifname, if_conf):          if 'policy' in if_conf: @@ -52,7 +52,7 @@ def get_policy_interfaces(conf, policy, name=None, ipv6=False):  def get_config_policy(conf, name=None, ipv6=False, interfaces=True):      config_path = ['policy']      if name: -        config_path += ['ipv6-route' if ipv6 else 'route', name] +        config_path += ['route6' if ipv6 else 'route', name]      policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),                                  get_first_key=True, no_tag_node_value_mangle=True) @@ -64,8 +64,8 @@ def get_config_policy(conf, name=None, ipv6=False, interfaces=True):                  for route_name, route_conf in policy['route'].items():                      route_conf['interface'] = [] -            if 'ipv6_route' in policy: -                for route_name, route_conf in policy['ipv6_route'].items(): +            if 'route6' in policy: +                for route_name, route_conf in policy['route6'].items():                      route_conf['interface'] = []          get_policy_interfaces(conf, policy, name, ipv6) @@ -151,8 +151,8 @@ def show_policy(ipv6=False):          for route, route_conf in policy['route'].items():              output_policy_route(route, route_conf, ipv6=False) -    if ipv6 and 'ipv6_route' in policy: -        for route, route_conf in policy['ipv6_route'].items(): +    if ipv6 and 'route6' in policy: +        for route, route_conf in policy['route6'].items():              output_policy_route(route, route_conf, ipv6=True)  def show_policy_name(name, ipv6=False): diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 679b03c0b..fd4f86d88 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -33,10 +33,12 @@ def utc2local(datetime):  def parse_time(s):      try: -        if re.match(r'^\d{1,2}$', s): -            if (int(s) > 59): +        if re.match(r'^\d{1,9999}$', s): +            if (int(s) > 59) and (int(s) < 1440):                  s = str(int(s)//60) + ":" + str(int(s)%60)                  return datetime.strptime(s, "%H:%M").time() +            if (int(s) >= 1440): +                return s.split()              else:                  return datetime.strptime(s, "%M").time()          else: @@ -141,7 +143,7 @@ def execute_shutdown(time, reboot=True, ask=True):              cmd(f'/usr/bin/wall "{wall_msg}"')          else:              if not ts: -                exit(f'Invalid time "{time[0]}". The valid format is HH:MM') +                exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format')              else:                  exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')      else: @@ -172,7 +174,12 @@ def main():      action.add_argument("--reboot", "-r",                          help="Reboot the system",                          nargs="*", -                        metavar="Minutes|HH:MM") +                        metavar="HH:MM") + +    action.add_argument("--reboot_in", "-i", +                        help="Reboot the system", +                        nargs="*", +                        metavar="Minutes")      action.add_argument("--poweroff", "-p",                          help="Poweroff the system", @@ -190,7 +197,17 @@ def main():      try:          if args.reboot is not None: +            for r in args.reboot: +                if ':' not in r and '/' not in r and '.' not in r: +                    print("Incorrect  format! Use HH:MM") +                    exit(1)              execute_shutdown(args.reboot, reboot=True, ask=args.yes) +        if args.reboot_in is not None: +            for i in args.reboot_in: +                if ':' in i: +                    print("Incorrect format! Use Minutes") +                    exit(1) +            execute_shutdown(args.reboot_in, reboot=True, ask=args.yes)          if args.poweroff is not None:              execute_shutdown(args.poweroff, reboot=False, ask=args.yes)          if args.cancel: diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py index 0040e950d..9973d9789 100755 --- a/src/op_mode/show_cpu.py +++ b/src/op_mode/show_cpu.py @@ -21,7 +21,7 @@ from sys import exit  from vyos.util import popen, DEVNULL  OUT_TMPL_SRC = """ -{% if cpu %} +{%- if cpu -%}  {% if 'vendor' in cpu %}CPU Vendor:       {{cpu.vendor}}{% endif %}  {% if 'model' in cpu %}Model:            {{cpu.model}}{% endif %}  {% if 'cpus' in cpu %}Total CPUs:       {{cpu.cpus}}{% endif %} @@ -31,31 +31,42 @@ OUT_TMPL_SRC = """  {% if 'mhz' in cpu %}Current MHz:      {{cpu.mhz}}{% endif %}  {% if 'mhz_min' in cpu %}Minimum MHz:      {{cpu.mhz_min}}{% endif %}  {% if 'mhz_max' in cpu %}Maximum MHz:      {{cpu.mhz_max}}{% endif %} -{% endif %} +{%- endif -%}  """ -cpu = {} -cpu_json, code = popen('lscpu -J', stderr=DEVNULL) - -if code == 0: -    cpu_info = json.loads(cpu_json) -    if len(cpu_info) > 0 and 'lscpu' in cpu_info: -        for prop in cpu_info['lscpu']: -            if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data'] -            if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data'] -            if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data'] -            if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data'] -            if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data'] -            if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data'] -            if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data'] -            if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data'] -            if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data'] - -if len(cpu) > 0: -    tmp = { 'cpu':cpu } +def get_raw_data(): +    cpu = {} +    cpu_json, code = popen('lscpu -J', stderr=DEVNULL) + +    if code == 0: +        cpu_info = json.loads(cpu_json) +        if len(cpu_info) > 0 and 'lscpu' in cpu_info: +            for prop in cpu_info['lscpu']: +                if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data'] +                if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data'] +                if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data'] +                if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data'] +                if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data'] +                if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data'] +                if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data'] +                if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data'] +                if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data'] + +    return cpu + +def get_formatted_output(): +    cpu = get_raw_data() + +    tmp = {'cpu':cpu}      tmpl = Template(OUT_TMPL_SRC) -    print(tmpl.render(tmp)) -    exit(0) -else: -    print('CPU information could not be determined\n') -    exit(1) +    return tmpl.render(tmp) + +if __name__ == '__main__': +    cpu = get_raw_data() + +    if len(cpu) > 0: +        print(get_formatted_output()) +    else: +        print('CPU information could not be determined\n') +        exit(1) + diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py index e72f0f965..5b8f00dba 100755 --- a/src/op_mode/show_ipsec_sa.py +++ b/src/op_mode/show_ipsec_sa.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -14,119 +14,117 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -import re -import sys +from re import split as re_split +from sys import exit -import vici -import tabulate -import hurry.filesize +from hurry import filesize +from tabulate import tabulate +from vici import Session as vici_session + +from vyos.util import seconds_to_human -import vyos.util  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))] +    return [convert(c) for c in re_split('([0-9]+)', str(key))] -def format_output(conns, sas): + +def format_output(sas):      sa_data = [] -    for peer, parent_conn in conns.items(): -        if peer not in sas: -            continue - -        parent_sa = sas[peer] -        child_sas = parent_sa['child-sas'] -        installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} - -        # parent_sa["state"] = IKE state, child_sas["state"] = ESP state -        state = 'down' -        uptime = 'N/A' - -        if parent_sa["state"] == b"ESTABLISHED" and installed_sas: -            state = "up" - -        remote_host = parent_sa["remote-host"].decode() -        remote_id = parent_sa["remote-id"].decode() - -        if remote_host == remote_id: -            remote_id = "N/A" - -        # The counters can only be obtained from the child SAs -        for child_conn in parent_conn['children']: -            if child_conn not in installed_sas: -                data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] -                sa_data.append(data) -                continue - -            isa = installed_sas[child_conn] -            csa_name = isa['name'] -            csa_name = csa_name.decode() - -            bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) -            bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) -            bytes_str = "{0}/{1}".format(bytes_in, bytes_out) - -            pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) -            pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) -            pkts_str = "{0}/{1}".format(pkts_in, pkts_out) -            # Remove B from <1K values -            pkts_str = re.sub(r'B', r'', pkts_str) - -            uptime = vyos.util.seconds_to_human(isa['install-time'].decode()) - -            enc = isa["encr-alg"].decode() -            if "encr-keysize" in isa: -                key_size = isa["encr-keysize"].decode() -            else: -                key_size = "" -            if "integ-alg" in isa: -                hash = isa["integ-alg"].decode() -            else: -                hash = "" -            if "dh-group" in isa: -                dh_group = isa["dh-group"].decode() -            else: -                dh_group = "" - -            proposal = enc -            if key_size: -                proposal = "{0}_{1}".format(proposal, key_size) -            if hash: -                proposal = "{0}/{1}".format(proposal, hash) -            if dh_group: -                proposal = "{0}/{1}".format(proposal, dh_group) - -            data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] -            sa_data.append(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.decode() +                if sa_state: +                    if sa_state == b'INSTALLED': +                        sa_out_state = 'up' +                    else: +                        sa_out_state = 'down' +                if sa_uptime: +                    sa_out_uptime = seconds_to_human(sa_uptime.decode()) +                if sa_bytes_in and sa_bytes_out: +                    bytes_in = filesize.size(int(sa_bytes_in.decode())) +                    bytes_out = filesize.size(int(sa_bytes_out.decode())) +                    sa_out_bytes = f'{bytes_in}/{bytes_out}' +                if sa_packets_in and sa_packets_out: +                    packets_in = filesize.size(int(sa_packets_in.decode()), +                                               system=filesize.si) +                    packets_out = filesize.size(int(sa_packets_out.decode()), +                                                system=filesize.si) +                    sa_out_packets = f'{packets_in}/{packets_out}' +                if sa_remote_addr: +                    sa_out_remote_addr = sa_remote_addr.decode() +                if sa_remote_id: +                    sa_out_remote_id = sa_remote_id.decode() +                # format proposal +                if sa_proposal_encr_alg: +                    sa_out_proposal = sa_proposal_encr_alg.decode() +                if sa_proposal_encr_keysize: +                    sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode() +                    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.decode() +                    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.decode() +                    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 +                ]) + +    # return output data      return sa_data +  if __name__ == '__main__':      try: -        session = vici.Session() -        conns = {} -        sas = {} +        session = vici_session() +        sas = list(session.list_sas()) -        for conn in session.list_conns(): -            for key in conn: -                conns[key] = conn[key] - -        for sa in session.list_sas(): -            for key in sa: -                sas[key] = sa[key] - -        headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] -        sa_data = format_output(conns, sas) +        sa_data = format_output(sas)          sa_data = sorted(sa_data, key=alphanum_key) -        output = tabulate.tabulate(sa_data, headers) + +        headers = [ +            "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", +            "Remote address", "Remote ID", "Proposal" +        ] +        output = tabulate(sa_data, headers)          print(output)      except PermissionError:          print("You do not have a permission to connect to the IPsec daemon") -        sys.exit(1) +        exit(1)      except ConnectionRefusedError:          print("IPsec is not runing") -        sys.exit(1) +        exit(1)      except Exception as e:          print("An error occured: {0}".format(e)) -        sys.exit(1) +        exit(1) diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py index 5818ec132..2b0be3965 100755 --- a/src/op_mode/show_ram.py +++ b/src/op_mode/show_ram.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -55,10 +55,17 @@ def get_system_memory_human():      return mem -if __name__ == '__main__': -    mem = get_system_memory_human() +def get_raw_data(): +    return get_system_memory_human() + +def get_formatted_output(): +    mem = get_raw_data() -    print("Total: {}".format(mem["total"])) -    print("Free:  {}".format(mem["free"])) -    print("Used:  {}".format(mem["used"])) +    out = "Total: {}\n".format(mem["total"]) +    out += "Free:  {}\n".format(mem["free"]) +    out += "Used:  {}".format(mem["used"]) +    return out + +if __name__ == '__main__': +    print(get_formatted_output()) diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py index c3dea52e6..1b5e33fa9 100755 --- a/src/op_mode/show_uptime.py +++ b/src/op_mode/show_uptime.py @@ -37,14 +37,27 @@ def get_load_averages():      return res -if __name__ == '__main__': +def get_raw_data():      from vyos.util import seconds_to_human -    print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds()))) +    res = {} +    res["uptime_seconds"] = get_uptime_seconds() +    res["uptime"] = seconds_to_human(get_uptime_seconds()) +    res["load_average"] = get_load_averages() + +    return res -    avgs = get_load_averages() +def get_formatted_output(): +    data = get_raw_data() -    print("Load averages:") -    print("1  minute:   {:.02f}%".format(avgs[1]*100)) -    print("5  minutes:  {:.02f}%".format(avgs[5]*100)) -    print("15 minutes:  {:.02f}%".format(avgs[15]*100)) +    out = "Uptime: {}\n\n".format(data["uptime"]) +    avgs = data["load_average"] +    out += "Load averages:\n" +    out += "1  minute:   {:.02f}%\n".format(avgs[1]*100) +    out += "5  minutes:  {:.02f}%\n".format(avgs[5]*100) +    out += "15 minutes:  {:.02f}%\n".format(avgs[15]*100) + +    return out + +if __name__ == '__main__': +    print(get_formatted_output()) diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py index 7962e1e7b..b82ab6eca 100755 --- a/src/op_mode/show_version.py +++ b/src/op_mode/show_version.py @@ -26,10 +26,6 @@ from jinja2 import Template  from sys import exit  from vyos.util import call -parser = argparse.ArgumentParser() -parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") -parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") -  version_output_tmpl = """  Version:          VyOS {{version}}  Release train:    {{release_train}} @@ -51,7 +47,20 @@ Hardware UUID:    {{hardware_uuid}}  Copyright:        VyOS maintainers and contributors  """ +def get_raw_data(): +    version_data = vyos.version.get_full_version_data() +    return version_data + +def get_formatted_output(): +    version_data = get_raw_data() +    tmpl = Template(version_output_tmpl) +    return tmpl.render(version_data) +  if __name__ == '__main__': +    parser = argparse.ArgumentParser() +    parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") +    parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") +      args = parser.parse_args()      version_data = vyos.version.get_full_version_data() @@ -60,9 +69,8 @@ if __name__ == '__main__':          import json          print(json.dumps(version_data))          exit(0) - -    tmpl = Template(version_output_tmpl) -    print(tmpl.render(version_data)) +    else: +        print(get_formatted_output())      if args.funny:          print(vyos.limericks.get_random()) diff --git a/src/op_mode/show_virtual_server.py b/src/op_mode/show_virtual_server.py new file mode 100755 index 000000000..377180dec --- /dev/null +++ b/src/op_mode/show_virtual_server.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.configquery import ConfigTreeQuery +from vyos.util import call + +def is_configured(): +    """ Check if high-availability virtual-server is configured """ +    config = ConfigTreeQuery() +    if not config.exists(['high-availability', 'virtual-server']): +        return False +    return True + +if __name__ == '__main__': + +    if is_configured() == False: +        print('Virtual server not configured!') +        exit(0) + +    call('sudo ipvsadm --list --numeric') diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 2c1db20bf..dab146d28 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -23,6 +23,7 @@ import tabulate  import vyos.util +from vyos.configquery import ConfigTreeQuery  from vyos.ifconfig.vrrp import VRRP  from vyos.ifconfig.vrrp import VRRPError, VRRPNoData @@ -35,7 +36,17 @@ group.add_argument("-d", "--data", action="store_true", help="Print detailed VRR  args = parser.parse_args() +def is_configured(): +    """ Check if VRRP is configured """ +    config = ConfigTreeQuery() +    if not config.exists(['high-availability', 'vrrp', 'group']): +        return False +    return True +  # Exit early if VRRP is dead or not configured +if  is_configured() == False: +    print('VRRP not configured!') +    exit(0)  if not VRRP.is_running():      print('VRRP is not running')      sys.exit(0) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 06871f1d6..c1b595412 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -352,7 +352,7 @@ class MultipartRoute(APIRoute):                  return error(e.status_code, e.detail)              except Exception as e:                  if request.ERR_MISSING_KEY: -                    return error(422, "Valid API key is required") +                    return error(401, "Valid API key is required")                  if request.ERR_MISSING_DATA:                      return error(422, "Non-empty data field is required")                  if request.ERR_NOT_JSON: @@ -648,10 +648,12 @@ if __name__ == '__main__':      app.state.vyos_keys = server_config['api_keys']      app.state.vyos_debug = server_config['debug'] +    app.state.vyos_gql = server_config['gql']      app.state.vyos_strict = server_config['strict']      app.state.vyos_origins = server_config.get('cors', {}).get('origins', []) -    graphql_init(app) +    if app.state.vyos_gql: +        graphql_init(app)      try:          if not server_config['socket']: diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index b1fe7e43f..a8df232ae 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -71,7 +71,8 @@ class KeepalivedFifo:              # Read VRRP configuration directly from CLI              self.vrrp_config_dict = conf.get_config_dict(base, -                                     key_mangling=('-', '_'), get_first_key=True) +                                     key_mangling=('-', '_'), get_first_key=True, +                                     no_tag_node_value_mangle=True)              logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')          except Exception as err: diff --git a/src/systemd/miniupnpd.service b/src/systemd/miniupnpd.service new file mode 100644 index 000000000..51cb2eed8 --- /dev/null +++ b/src/systemd/miniupnpd.service @@ -0,0 +1,13 @@ +[Unit] +Description=UPnP service +ConditionPathExists=/run/upnp/miniupnp.conf +After=vyos-router.service +StartLimitIntervalSec=0 + +[Service] +WorkingDirectory=/run/upnp +Type=simple +ExecStart=/usr/sbin/miniupnpd -d -f /run/upnp/miniupnp.conf +PrivateTmp=yes +PIDFile=/run/miniupnpd.pid +Restart=on-failure diff --git a/src/tests/test_util.py b/src/tests/test_util.py index 9bd27adc0..8ac9a500a 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -23,3 +23,6 @@ class TestVyOSUtil(TestCase):          expected_data = {"foo_bar": {"baz_quux": None}}          new_data = mangle_dict_keys(data, '-', '_')          self.assertEqual(new_data, expected_data) + +    def test_sysctl_read(self): +        self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') diff --git a/src/validators/ip-address b/src/validators/ip-address index 51fb72c85..11d6df09e 100755 --- a/src/validators/ip-address +++ b/src/validators/ip-address @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-any-single $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IP address" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr index 987bf84ca..60d2ac295 100755 --- a/src/validators/ip-cidr +++ b/src/validators/ip-cidr @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-any-cidr $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IP CIDR" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-host b/src/validators/ip-host index f2906e8cf..77c578fa2 100755 --- a/src/validators/ip-host +++ b/src/validators/ip-host @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-any-host $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IP host" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix index e58aad395..e5a64fea8 100755 --- a/src/validators/ip-prefix +++ b/src/validators/ip-prefix @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-any-net $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IP prefix" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol index 7898fa6d0..c4c882502 100755 --- a/src/validators/ip-protocol +++ b/src/validators/ip-protocol @@ -38,4 +38,5 @@ if __name__ == '__main__':      if re.match(pattern, input):          exit(0) +    print(f'Error: {input} is not a valid IP protocol')      exit(1) diff --git a/src/validators/ipv4 b/src/validators/ipv4 index 53face090..8676d5800 100755 --- a/src/validators/ipv4 +++ b/src/validators/ipv4 @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv4 $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not IPv4" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address index 872a7645a..058db088b 100755 --- a/src/validators/ipv4-address +++ b/src/validators/ipv4-address @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv4-single $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv4 address" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host index f42feffa4..74b8c36a7 100755 --- a/src/validators/ipv4-host +++ b/src/validators/ipv4-host @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv4-host $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv4 host" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast index 5465c728d..3f28c51db 100755 --- a/src/validators/ipv4-multicast +++ b/src/validators/ipv4-multicast @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv4-multicast $1 && ipaddrcheck --is-ipv4-single $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv4 multicast address" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix index 8ec8a2c45..7e1e0e8dd 100755 --- a/src/validators/ipv4-prefix +++ b/src/validators/ipv4-prefix @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv4-net $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv4 prefix" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range index cc59039f1..6492bfc52 100755 --- a/src/validators/ipv4-range +++ b/src/validators/ipv4-range @@ -7,6 +7,11 @@ ip2dec () {      printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"  } +error_exit() { +  echo "Error: $1 is not a valid IPv4 address range" +  exit 1 +} +  # Only run this if there is a hypen present in $1  if [[ "$1" =~ "-" ]]; then    # This only works with real bash (<<<) - split IP addresses into array with @@ -15,21 +20,21 @@ if [[ "$1" =~ "-" ]]; then    ipaddrcheck --is-ipv4-single ${strarr[0]}    if [ $? -gt 0 ]; then -    exit 1 +    error_exit $1    fi    ipaddrcheck --is-ipv4-single ${strarr[1]}    if [ $? -gt 0 ]; then -    exit 1 +    error_exit $1    fi    start=$(ip2dec ${strarr[0]})    stop=$(ip2dec ${strarr[1]})    if [ $start -ge $stop ]; then -    exit 1 +    error_exit $1    fi    exit 0  fi -exit 1 +error_exit $1 diff --git a/src/validators/ipv6 b/src/validators/ipv6 index f18d4a63e..4ae130eb5 100755 --- a/src/validators/ipv6 +++ b/src/validators/ipv6 @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv6 $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not IPv6" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address index e5d68d756..1fca77668 100755 --- a/src/validators/ipv6-address +++ b/src/validators/ipv6-address @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv6-single $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv6 address" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host index f7a745077..7085809a9 100755 --- a/src/validators/ipv6-host +++ b/src/validators/ipv6-host @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv6-host $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv6 host" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast index 5afc437e5..5aa7d734a 100755 --- a/src/validators/ipv6-multicast +++ b/src/validators/ipv6-multicast @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv6-multicast $1 && ipaddrcheck --is-ipv6-single $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv6 multicast address" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix index e43616350..890dda723 100755 --- a/src/validators/ipv6-prefix +++ b/src/validators/ipv6-prefix @@ -1,3 +1,10 @@  #!/bin/sh  ipaddrcheck --is-ipv6-net $1 + +if [ $? -gt 0 ]; then +    echo "Error: $1 is not a valid IPv6 prefix" +    exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range index 033b6461b..7080860c4 100755 --- a/src/validators/ipv6-range +++ b/src/validators/ipv6-range @@ -1,16 +1,20 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 -import sys -import re -from vyos.template import is_ipv6 +from ipaddress import IPv6Address +from sys import argv, exit  if __name__ == '__main__': -    if len(sys.argv)>1: -        ipv6_range = sys.argv[1] -        # Regex for ipv6-ipv6 https://regexr.com/ -        if re.search('([a-f0-9:]+:+)+[a-f0-9]+-([a-f0-9:]+:+)+[a-f0-9]+', ipv6_range): -            for tmp in ipv6_range.split('-'): -                if not is_ipv6(tmp): -                    sys.exit(1) - -    sys.exit(0) +    if len(argv) > 1: +        # try to pass validation and raise an error if failed +        try: +            ipv6_range = argv[1] +            range_left = ipv6_range.split('-')[0] +            range_right = ipv6_range.split('-')[1] +            if not IPv6Address(range_left) < IPv6Address(range_right): +                raise ValueError(f'left element {range_left} must be less than right element {range_right}') +        except Exception as err: +            print(f'Error: {ipv6_range} is not a valid IPv6 range: {err}') +            exit(1) +    else: +        print('Error: an IPv6 range argument must be provided') +        exit(1) diff --git a/src/validators/mac-address-firewall b/src/validators/mac-address-firewall new file mode 100755 index 000000000..70551f86d --- /dev/null +++ b/src/validators/mac-address-firewall @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +pattern = "^!?([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" + +if __name__ == '__main__': +    if len(sys.argv) != 2: +        sys.exit(1) +    if not re.match(pattern, sys.argv[1]): +        sys.exit(1) +    sys.exit(0) diff --git a/src/validators/port-multi b/src/validators/port-multi new file mode 100755 index 000000000..cef371563 --- /dev/null +++ b/src/validators/port-multi @@ -0,0 +1,45 @@ +#!/usr/bin/python3 + +import sys +import re + +from vyos.util import read_file + +services_file = '/etc/services' + +def get_services(): +    names = [] +    service_data = read_file(services_file, "") +    for line in service_data.split("\n"): +        if not line or line[0] == '#': +            continue +        names.append(line.split(None, 1)[0]) +    return names + +if __name__ == '__main__': +    if len(sys.argv)>1: +        ports = sys.argv[1].split(",") +        services = get_services() + +        for port in ports: +            if port and port[0] == '!': +                port = port[1:] +            if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port): +                port_1, port_2 = port.split('-') +                if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536): +                    print(f'Error: {port} is not a valid port range') +                    sys.exit(1) +                if int(port_1) > int(port_2): +                    print(f'Error: {port} is not a valid port range') +                    sys.exit(1) +            elif port.isnumeric(): +                if int(port) not in range(1, 65536): +                    print(f'Error: {port} is not a valid port') +                    sys.exit(1) +            elif port not in services: +                print(f'Error: {port} is not a valid service name') +                sys.exit(1) +    else: +        sys.exit(2) + +    sys.exit(0) diff --git a/src/validators/port-range b/src/validators/port-range index abf0b09d5..5468000a7 100755 --- a/src/validators/port-range +++ b/src/validators/port-range @@ -3,16 +3,37 @@  import sys  import re +from vyos.util import read_file + +services_file = '/etc/services' + +def get_services(): +    names = [] +    service_data = read_file(services_file, "") +    for line in service_data.split("\n"): +        if not line or line[0] == '#': +            continue +        names.append(line.split(None, 1)[0]) +    return names + +def error(port_range): +    print(f'Error: {port_range} is not a valid port or port range') +    sys.exit(1) +  if __name__ == '__main__':      if len(sys.argv)>1:          port_range = sys.argv[1] -        if re.search('[0-9]{1,5}-[0-9]{1,5}', port_range): -            for tmp in port_range.split('-'): -                if int(tmp) not in range(1, 65535): -                    sys.exit(1) -        else: -            if int(port_range) not in range(1, 65535): -                sys.exit(1) +        if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port_range): +            port_1, port_2 = port_range.split('-') +            if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536): +                error(port_range) +            if int(port_1) > int(port_2): +                error(port_range) +        elif port_range.isnumeric() and int(port_range) not in range(1, 65536): +            error(port_range) +        elif not port_range.isnumeric() and port_range not in get_services(): +            print(f'Error: {port_range} is not a valid service name') +            sys.exit(1)      else:          sys.exit(2) diff --git a/src/validators/tcp-flag b/src/validators/tcp-flag new file mode 100755 index 000000000..1496b904a --- /dev/null +++ b/src/validators/tcp-flag @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +import sys +import re + +if __name__ == '__main__': +    if len(sys.argv)>1: +        flag = sys.argv[1] +        if flag and flag[0] == '!': +            flag = flag[1:] +        if flag not in ['syn', 'ack', 'rst', 'fin', 'urg', 'psh', 'ecn', 'cwr']: +            print(f'Error: {flag} is not a valid TCP flag') +            sys.exit(1) +    else: +        sys.exit(2) + +    sys.exit(0) | 
