diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/dns_forwarding.py | 22 | ||||
| -rwxr-xr-x | src/conf_mode/load-balancing-wan.py | 132 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_bgp.py | 38 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_isis.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_ospf.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_ospfv3.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/vrf.py | 14 | ||||
| -rw-r--r-- | src/conf_mode/vrf_vni.py | 104 | ||||
| -rwxr-xr-x | src/helpers/vyos-failover.py | 41 | ||||
| -rwxr-xr-x | src/op_mode/bgp.py | 170 | ||||
| -rwxr-xr-x | src/op_mode/conntrack_sync.py | 219 | ||||
| -rwxr-xr-x | src/op_mode/dynamic_dns.py | 16 | ||||
| -rwxr-xr-x | src/op_mode/ipsec.py | 216 | ||||
| -rwxr-xr-x | src/op_mode/show_vpn_ra.py | 56 | ||||
| -rwxr-xr-x | src/op_mode/show_wwan.py | 8 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 2 | ||||
| -rw-r--r-- | src/systemd/vyos-wan-load-balance.service | 15 | 
17 files changed, 775 insertions, 284 deletions
| diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 36c1098fe..0d86c6a52 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -99,7 +99,7 @@ def get_config(config=None):              recorddata = zonedata['records'] -            for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]: +            for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ns', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:                  if rtype not in recorddata:                      continue                  for subnode in recorddata[rtype]: @@ -113,7 +113,7 @@ def get_config(config=None):                          rdata = dict_merge(rdefaults, rdata)                          if not 'address' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required')                              continue                          if subnode == 'any': @@ -126,12 +126,12 @@ def get_config(config=None):                                  'ttl': rdata['ttl'],                                  'value': address                              }) -                    elif rtype in ['cname', 'ptr']: +                    elif rtype in ['cname', 'ptr', 'ns']:                          rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665                          rdata = dict_merge(rdefaults, rdata)                          if not 'target' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: target is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required')                              continue                          zone['records'].append({ @@ -146,7 +146,7 @@ def get_config(config=None):                          rdata = dict_merge(rdefaults, rdata)                          if not 'server' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: at least one server is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required')                              continue                          for servername in rdata['server']: @@ -164,7 +164,7 @@ def get_config(config=None):                          rdata = dict_merge(rdefaults, rdata)                          if not 'value' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: at least one value is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required')                              continue                          for value in rdata['value']: @@ -179,7 +179,7 @@ def get_config(config=None):                          rdata = dict_merge(rdefaults, rdata)                          if not 'value' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: value is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required')                              continue                          zone['records'].append({ @@ -194,7 +194,7 @@ def get_config(config=None):                          rdata = dict_merge(rdefaults, rdata)                          if not 'entry' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: at least one entry is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required')                              continue                          for entryno in rdata['entry']: @@ -203,11 +203,11 @@ def get_config(config=None):                              entrydata = dict_merge(entrydefaults, entrydata)                              if not 'hostname' in entrydata: -                                dns['authoritative_zone_errors'].append('{}.{}: hostname is required for entry {}'.format(subnode, node, entryno)) +                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}')                                  continue                              if not 'port' in entrydata: -                                dns['authoritative_zone_errors'].append('{}.{}: port is required for entry {}'.format(subnode, node, entryno)) +                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: port is required for entry {entryno}')                                  continue                              zone['records'].append({ @@ -223,7 +223,7 @@ def get_config(config=None):                          if not 'rule' in rdata: -                            dns['authoritative_zone_errors'].append('{}.{}: at least one rule is required'.format(subnode, node)) +                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required')                              continue                          for ruleno in rdata['rule']: diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py index 11840249f..7086aaf8b 100755 --- a/src/conf_mode/load-balancing-wan.py +++ b/src/conf_mode/load-balancing-wan.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 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,17 +14,25 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. +import os  from sys import exit +from shutil import rmtree +from vyos.base import Warning  from vyos.config import Config -from vyos.configdict import node_changed -from vyos.util import call +from vyos.configdict import dict_merge +from vyos.util import cmd +from vyos.template import render +from vyos.xml import defaults  from vyos import ConfigError -from pprint import pprint  from vyos import airbag  airbag.enable() +load_balancing_dir = '/run/load-balance' +load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' +systemd_service = 'vyos-wan-load-balance.service' +  def get_config(config=None):      if config: @@ -33,27 +41,135 @@ def get_config(config=None):          conf = Config()      base = ['load-balancing', 'wan'] -    lb = conf.get_config_dict(base, get_first_key=True, -                                       no_tag_node_value_mangle=True) +    lb = conf.get_config_dict(base, +                              get_first_key=True, +                              key_mangling=('-', '_'), +                              no_tag_node_value_mangle=True) + +    # We have gathered the dict representation of the CLI, but there are default +    # options which we need to update into the dictionary retrived. +    default_values = defaults(base) +    # lb base default values can not be merged here - remove and add them later +    if 'interface_health' in default_values: +        del default_values['interface_health'] +    if 'rule' in default_values: +        del default_values['rule'] +    lb = dict_merge(default_values, lb) + +    if 'interface_health' in lb: +        for iface in lb.get('interface_health'): +            default_values_iface = defaults(base + ['interface-health']) +            if 'test' in default_values_iface: +                del default_values_iface['test'] +            lb['interface_health'][iface] = dict_merge( +                default_values_iface, lb['interface_health'][iface]) +            if 'test' in lb['interface_health'][iface]: +                for node_test in lb['interface_health'][iface]['test']: +                    default_values_test = defaults(base + +                                                   ['interface-health', 'test']) +                    lb['interface_health'][iface]['test'][node_test] = dict_merge( +                            default_values_test, +                            lb['interface_health'][iface]['test'][node_test]) + +    if 'rule' in lb: +        for rule in lb.get('rule'): +            default_values_rule = defaults(base + ['rule']) +            if 'interface' in default_values_rule: +                del default_values_rule['interface'] +            lb['rule'][rule] = dict_merge(default_values_rule, lb['rule'][rule]) +            if not conf.exists(base + ['rule', rule, 'limit']): +                del lb['rule'][rule]['limit'] +            if 'interface' in lb['rule'][rule]: +                for iface in lb['rule'][rule]['interface']: +                    default_values_rule_iface = defaults(base + ['rule', 'interface']) +                    lb['rule'][rule]['interface'][iface] = dict_merge(default_values_rule_iface, lb['rule'][rule]['interface'][iface]) -    pprint(lb)      return lb +  def verify(lb): -    return None +    if not lb: +        return None + +    if 'interface_health' not in lb: +        raise ConfigError( +            'A valid WAN load-balance configuration requires an interface with a nexthop!' +        ) + +    for interface, interface_config in lb['interface_health'].items(): +        if 'nexthop' not in interface_config: +            raise ConfigError( +                f'interface-health {interface} nexthop must be specified!') + +        if 'test' in interface_config: +            for test_rule, test_config in interface_config['test'].items(): +                if 'type' in test_config: +                    if test_config['type'] == 'user-defined' and 'test_script' not in test_config: +                        raise ConfigError( +                            f'test {test_rule} script must be defined for test-script!' +                        ) + +    if 'rule' not in lb: +        Warning( +            'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' +        ) +    else: +        for rule, rule_config in lb['rule'].items(): +            if 'inbound_interface' not in rule_config: +                raise ConfigError(f'rule {rule} inbound-interface must be specified!') +            if {'failover', 'exclude'} <= set(rule_config): +                raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') +            if {'limit', 'exclude'} <= set(rule_config): +                raise ConfigError(f'rule {rule} limit cannot be used with exclude!') +            if 'interface' not in rule_config: +                if 'exclude' not in rule_config: +                    Warning( +                        f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' +                    ) +            for direction in {'source', 'destination'}: +                if direction in rule_config: +                    if 'protocol' in rule_config and 'port' in rule_config[ +                            direction]: +                        if rule_config['protocol'] not in {'tcp', 'udp'}: +                            raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')  def generate(lb):      if not lb: +        # Delete /run/load-balance/wlb.conf +        if os.path.isfile(load_balancing_conf_file): +            os.unlink(load_balancing_conf_file) +        # Delete old directories +        if os.path.isdir(load_balancing_dir): +            rmtree(load_balancing_dir, ignore_errors=True) +        if os.path.exists('/var/run/load-balance/wlb.out'): +            os.unlink('/var/run/load-balance/wlb.out') +          return None +    # Create load-balance dir +    if not os.path.isdir(load_balancing_dir): +        os.mkdir(load_balancing_dir) + +    render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) +      return None  def apply(lb): +    if not lb: +        try: +            cmd(f'systemctl stop {systemd_service}') +        except Exception as e: +            print(f"Error message: {e}") + +    else: +        cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') +        cmd(f'systemctl restart {systemd_service}')      return None +  if __name__ == '__main__':      try:          c = get_config() diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 66505e58d..b23584bdb 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -50,16 +50,24 @@ def get_config(config=None):      bgp = conf.get_config_dict(base, key_mangling=('-', '_'),                                 get_first_key=True, no_tag_node_value_mangle=True) -    # Assign the name of our VRF context. This MUST be done before the return -    # statement below, else on deletion we will delete the default instance -    # instead of the VRF instance. -    if vrf: bgp.update({'vrf' : vrf}) -      bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],                                                   key_mangling=('-', '_'),                                                   get_first_key=True,                                                   no_tag_node_value_mangle=True) +    # Assign the name of our VRF context. This MUST be done before the return +    # statement below, else on deletion we will delete the default instance +    # instead of the VRF instance. +    if vrf: +        bgp.update({'vrf' : vrf}) +        # We can not delete the BGP VRF instance if there is a L3VNI configured +        tmp = ['vrf', 'name', vrf, 'vni'] +        if conf.exists(tmp): +            bgp.update({'vni' : conf.return_value(tmp)}) +        # We can safely delete ourself from the dependent vrf list +        if vrf in bgp['dependent_vrfs']: +            del bgp['dependent_vrfs'][vrf] +      bgp['dependent_vrfs'].update({'default': {'protocols': {          'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'),                                      get_first_key=True, @@ -202,9 +210,13 @@ def verify(bgp):          if 'vrf' in bgp:              # Cannot delete vrf if it exists in import vrf list in other vrfs              for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']: -                if verify_vrf_as_import(bgp['vrf'],tmp_afi,bgp['dependent_vrfs']): -                    raise ConfigError(f'Cannot delete vrf {bgp["vrf"]} instance, ' \ -                                      'Please unconfigure import vrf commands!') +                if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']): +                    raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ +                                      'unconfigure "import vrf" commands!') +            # We can not delete the BGP instance if a L3VNI instance exists +            if 'vni' in bgp: +                raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ +                                  f'unconfigure VNI "{bgp["vni"]}" first!')          else:              # We are running in the default VRF context, thus we can not delete              # our main BGP instance if there are dependent BGP VRF instances. @@ -429,7 +441,6 @@ def verify(bgp):                                           f'{afi} administrative distance {key}!')              if afi in ['ipv4_unicast', 'ipv6_unicast']: -                  vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default'                  # Verify if currant VRF contains rd and route-target options                  # and does not exist in import list in other VRFs @@ -478,6 +489,15 @@ def verify(bgp):                      tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)                      if tmp: verify_route_map(tmp, bgp) +            # Checks only required for L2VPN EVPN +            if afi in ['l2vpn_evpn']: +                if 'vni' in afi_config: +                    for vni, vni_config in afi_config['vni'].items(): +                        if 'rd' in vni_config and 'advertise_all_vni' not in afi_config: +                            raise ConfigError('BGP EVPN "rd" requires "advertise-all-vni" to be set!') +                        if 'route_target' in vni_config and 'advertise_all_vni' not in afi_config: +                            raise ConfigError('BGP EVPN "route-target" requires "advertise-all-vni" to be set!') +      return None  def generate(bgp): diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index af2937db8..ecca87db0 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -129,7 +129,7 @@ def verify(isis):              vrf = isis['vrf']              tmp = get_interface_config(interface)              if 'master' not in tmp or tmp['master'] != vrf: -                raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') +                raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')      # If md5 and plaintext-password set at the same time      for password in ['area_password', 'domain_password']: diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index fbb876123..b73483470 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -196,7 +196,7 @@ def verify(ospf):                  vrf = ospf['vrf']                  tmp = get_interface_config(interface)                  if 'master' not in tmp or tmp['master'] != vrf: -                    raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') +                    raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')      # Segment routing checks      if dict_search('segment_routing.global_block', ospf): diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index ee1fdd399..cb21bd83c 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -138,7 +138,7 @@ def verify(ospfv3):                  vrf = ospfv3['vrf']                  tmp = get_interface_config(interface)                  if 'master' not in tmp or tmp['master'] != vrf: -                    raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') +                    raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')      return None diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index a7ef4cb5c..0b983293e 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -108,6 +108,12 @@ def get_config(config=None):      # vyos.configverify.verify_common_route_maps() for more information.      tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],                                                            get_first_key=True)}} + +    # L3VNI setup is done via vrf_vni.py as it must be de-configured (on node +    # deletetion prior to the BGP process. Tell the Jinja2 template no VNI +    # setup is needed +    vrf.update({'no_vni' : ''}) +      # Merge policy dict into "regular" config dict      vrf = dict_merge(tmp, vrf)      return vrf @@ -124,8 +130,8 @@ def verify(vrf):                                    f'static routes installed!')      if 'name' in vrf: -        reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type", -                          "vrf"] +        reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", +                          "get", "inet", "mtu", "link", "type", "vrf"]          table_ids = []          for name, vrf_config in vrf['name'].items():              # Reserved VRF names @@ -142,8 +148,8 @@ def verify(vrf):                  if tmp and tmp != vrf_config['table']:                      raise ConfigError(f'VRF "{name}" table id modification not possible!') -            # VRf routing table ID must be unique on the system -            if vrf_config['table'] in table_ids: +            # VRF routing table ID must be unique on the system +            if 'table' in vrf_config and vrf_config['table'] in table_ids:                  raise ConfigError(f'VRF "{name}" table id is not unique!')              table_ids.append(vrf_config['table']) diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py new file mode 100644 index 000000000..9f33536e5 --- /dev/null +++ b/src/conf_mode/vrf_vni.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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.config import Config +from vyos.template import render_to_string +from vyos.util import dict_search +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() + +    vrf_name = None +    if len(argv) > 1: +        vrf_name = argv[1] +    else: +        return None + +    # Using duplicate L3VNIs makes no sense - it's also forbidden in FRR, +    # thus VyOS CLI must deny this, too. Instead of getting only the dict for +    # the requested VRF and den comparing it with depenent VRfs to not have any +    # duplicate we will just grad ALL VRFs by default but only render/apply +    # the configuration for the requested VRF - that makes the code easier and +    # hopefully less error prone +    vrf = conf.get_config_dict(['vrf'], key_mangling=('-', '_'), +                               no_tag_node_value_mangle=True, +                               get_first_key=True) + +    # Store name of VRF we are interested in for FRR config rendering +    vrf.update({'only_vrf' : vrf_name}) + +    return vrf + +def verify(vrf): +    if not vrf: +        return + +    if len(argv) < 2: +        raise ConfigError('VRF parameter not specified when valling vrf_vni.py') + +    if 'name' in vrf: +        vni_ids = [] +        for name, vrf_config in vrf['name'].items(): +            # VRF VNI (Virtual Network Identifier) must be unique on the system +            if 'vni' in vrf_config: +                if vrf_config['vni'] in vni_ids: +                    raise ConfigError(f'VRF "{name}" VNI is not unique!') +                vni_ids.append(vrf_config['vni']) + +    return None + +def generate(vrf): +    if not vrf: +        return + +    vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) +    return None + +def apply(vrf): +    frr_daemon = 'zebra' + +    # add configuration to FRR +    frr_cfg = frr.FRRConfig() +    frr_cfg.load_configuration(frr_daemon) +    # There is only one VRF inside the dict as we read only one in get_config() +    if vrf and 'only_vrf' in vrf: +        vrf_name = vrf['only_vrf'] +        frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True) +    if vrf and 'new_frr_config' in vrf: +        frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) +    frr_cfg.commit_configuration(frr_daemon) + +    return None + +if __name__ == '__main__': +    try: +        c = get_config() +        verify(c) +        generate(c) +        apply(c) +    except ConfigError as e: +        print(e) +        exit(1) diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py index 03fb42f57..ce4cf8fa4 100755 --- a/src/helpers/vyos-failover.py +++ b/src/helpers/vyos-failover.py @@ -93,7 +93,12 @@ def is_port_open(ip, port):          s.close() -def is_target_alive(target_list=None, iface='', proto='icmp', port=None, debug=False): +def is_target_alive(target_list=None, +                    iface='', +                    proto='icmp', +                    port=None, +                    debug=False, +                    policy='any-available') -> bool:      """Check the availability of each target in the target_list using      the specified protocol ICMP, ARP, TCP @@ -103,17 +108,19 @@ def is_target_alive(target_list=None, iface='', proto='icmp', port=None, debug=F          proto (str): The protocol to use for the check. Options are 'icmp', 'arp', or 'tcp'.          port (int): The port number to use for the TCP check. Only applicable if proto is 'tcp'.          debug (bool): If True, print debug information during the check. +        policy (str): The policy to use for the check. Options are 'any-available' or 'all-available'.      Returns: -        bool: True if all targets are reachable, False otherwise. +        bool: True if all targets are reachable according to the policy, False otherwise.      Example: -        % is_target_alive(['192.0.2.1', '192.0.2.5'], 'eth1', proto='arp') +        % is_target_alive(['192.0.2.1', '192.0.2.5'], 'eth1', proto='arp', policy='all-available')          True      """      if iface != '':          iface = f'-I {iface}' +    num_reachable_targets = 0      for target in target_list:          match proto:              case 'icmp': @@ -121,25 +128,34 @@ def is_target_alive(target_list=None, iface='', proto='icmp', port=None, debug=F                  rc, response = rc_cmd(command)                  if debug:                      print(f'    [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') -                if rc != 0: -                    return False +                if rc == 0: +                    num_reachable_targets += 1 +                    if policy == 'any-available': +                        return True              case 'arp':                  command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}'                  rc, response = rc_cmd(command)                  if debug:                      print(f'    [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') -                if rc != 0: -                    return False +                if rc == 0: +                    num_reachable_targets += 1 +                    if policy == 'any-available': +                        return True              case _ if proto == 'tcp' and port is not None: -                if not is_port_open(target, port): -                    return False +                if is_port_open(target, port): +                    num_reachable_targets += 1 +                    if policy == 'any-available': +                        return True              case _:                  return False -    return True +        if policy == 'all-available' and num_reachable_targets == len(target_list): +            return True + +    return False  if __name__ == '__main__': @@ -178,6 +194,7 @@ if __name__ == '__main__':                  conf_metric = int(nexthop_config.get('metric'))                  port = nexthop_config.get('check').get('port')                  port_opt = f'port {port}' if port else '' +                policy = nexthop_config.get('check').get('policy')                  proto = nexthop_config.get('check').get('type')                  target = nexthop_config.get('check').get('target')                  timeout = nexthop_config.get('check').get('timeout') @@ -186,7 +203,7 @@ if __name__ == '__main__':                  if not is_route_exists(route, next_hop, conf_iface, conf_metric):                      if debug: print(f"    [NEW_ROUTE_DETECTED] route: [{route}]")                      # Add route if check-target alive -                    if is_target_alive(target, conf_iface, proto, port, debug=debug): +                    if is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy):                          if debug: print(f'    [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} '                                          f'metric {conf_metric} proto failover\n###')                          rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} ' @@ -205,7 +222,7 @@ if __name__ == '__main__':                  # Route was added, check if the target is alive                  # We should delete route if check fails only if route exists in the routing table -                if not is_target_alive(target, conf_iface, proto, port, debug=debug) and \ +                if not is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy) and \                          is_route_exists(route, next_hop, conf_iface, conf_metric):                      if debug:                          print(f'Nexh_hop {next_hop} fail, target not response') diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py index 3f6d45dd7..af9ea788b 100755 --- a/src/op_mode/bgp.py +++ b/src/op_mode/bgp.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 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,101 +15,133 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  #  # Purpose: -#    Displays bgp neighbors information. -#    Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands. +#    Displays BGP neighbors and tables information.  import re  import sys  import typing -import jmespath  from jinja2 import Template -from humps import decamelize - -from vyos.configquery import ConfigTreeQuery  import vyos.opmode -ArgFamily = typing.Literal['inet', 'inet6'] -  frr_command_template = Template(""" -{% if family %} -    show bgp -        {{ 'vrf ' ~ vrf if vrf else '' }} -        {{ 'ipv6' if family == 'inet6' else 'ipv4'}} -        {{ 'neighbor ' ~ peer if peer else 'summary' }} +show bgp + +{## VRF and family modifiers that may precede any options ##} + +{% if vrf %} +    vrf {{vrf}} +{% endif %} + +{% if family == "inet" %} +    ipv4 +{% elif family == "inet6" %} +    ipv6 +{% elif family == "l2vpn" %} +    l2vpn evpn +{% endif %} + +{% if family_modifier == "unicast" %} +    unicast +{% elif family_modifier == "multicast" %} +    multicast +{% elif family_modifier == "flowspec" %} +    flowspec +{% elif family_modifier == "vpn" %} +    vpn +{% endif %} + +{## Mutually exclusive query parameters ##} + +{# Network prefix #} +{% if prefix %} +    {{prefix}} + +    {% if longer_prefixes %} +      longer-prefixes +    {% elif best_path %} +      bestpath +    {% endif %}  {% endif %} +{# Regex #} +{% if regex %} +    regex {{regex}} +{% endif %} + +{## Raw modifier ##} +  {% if raw %}      json  {% endif %}  """) +ArgFamily = typing.Literal['inet', 'inet6', 'l2vpn'] +ArgFamilyModifier = typing.Literal['unicast', 'labeled_unicast', 'multicast', 'vpn', 'flowspec'] + +def show_summary(raw: bool): +    from vyos.util import cmd + +    if raw: +        from json import loads + +        output = cmd(f"vtysh -c 'show bgp summary json'").strip() -def _verify(func): -    """Decorator checks if BGP config exists -    BGP configuration can be present under vrf <tag> -    If we do npt get arg 'peer' then it can be 'bgp summary' -    """ -    from functools import wraps - -    @wraps(func) -    def _wrapper(*args, **kwargs): -        config = ConfigTreeQuery() -        afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4' -        global_vrfs = ['all', 'default'] -        peer = kwargs.get('peer') -        vrf = kwargs.get('vrf') -        unconf_message = f'BGP or neighbor is not configured' -        # Add option to check the specific neighbor if we have arg 'peer' -        peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else '' -        vrf_opt = '' -        if vrf and vrf not in global_vrfs: -            vrf_opt = f'vrf name {vrf}' -        # Check if config does not exist -        if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'): -            raise vyos.opmode.UnconfiguredSubsystem(unconf_message) -        return func(*args, **kwargs) - -    return _wrapper - - -@_verify -def show_neighbors(raw: bool, -                   family: ArgFamily, -                   peer: typing.Optional[str], -                   vrf: typing.Optional[str]): -    kwargs = dict(locals()) -    frr_command = frr_command_template.render(kwargs) -    frr_command = re.sub(r'\s+', ' ', frr_command) +        # FRR 8.5 correctly returns an empty object when BGP is not running, +        # we don't need to do anything special here +        return loads(output) +    else: +        output = cmd(f"vtysh -c 'show bgp summary'") +        return output +def show_neighbors(raw: bool):      from vyos.util import cmd -    output = cmd(f"vtysh -c '{frr_command}'") +    from vyos.utils.dict import dict_to_list      if raw:          from json import loads -        data = loads(output) -        # Get list of the peers -        peers = jmespath.search('*.peers | [0]', data) -        if peers: -            # Create new dict, delete old key 'peers' -            # add key 'peers' neighbors to the list -            list_peers = [] -            new_dict = jmespath.search('* | [0]', data) -            if 'peers' in new_dict: -                new_dict.pop('peers') - -                for neighbor, neighbor_options in peers.items(): -                    neighbor_options['neighbor'] = neighbor -                    list_peers.append(neighbor_options) -                new_dict['peers'] = list_peers -            return decamelize(new_dict) -        data = jmespath.search('* | [0]', data) -        return decamelize(data) +        output = cmd(f"vtysh -c 'show bgp neighbors json'").strip() +        d = loads(output) +        return dict_to_list(d, save_key_to="neighbor")      else: +        output = cmd(f"vtysh -c 'show bgp neighbors'")          return output +def show(raw: bool, +         family: ArgFamily, +         family_modifier: ArgFamilyModifier, +         prefix: typing.Optional[str], +         longer_prefixes: typing.Optional[bool], +         best_path: typing.Optional[bool], +         regex: typing.Optional[str], +         vrf: typing.Optional[str]): +    from vyos.utils.dict import dict_to_list + +    if (longer_prefixes or best_path) and (prefix is None): +        raise ValueError("longer_prefixes and best_path can only be used when prefix is given") +    elif (family == "l2vpn") and (family_modifier is not None): +        raise ValueError("l2vpn family does not accept any modifiers") +    else: +        kwargs = dict(locals()) + +        frr_command = frr_command_template.render(kwargs) +        frr_command = re.sub(r'\s+', ' ', frr_command) + +        from vyos.util import cmd +        output = cmd(f"vtysh -c '{frr_command}'") + +        if raw: +            from json import loads +            d = loads(output) +            if not ("routes" in d): +                raise vyos.opmode.InternalError("FRR returned a BGP table with no routes field") +            d = d["routes"] +            routes = dict_to_list(d, save_key_to="route_key") +            return routes +        else: +            return output  if __name__ == '__main__':      try: diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index 54ecd6d0e..c3345a936 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.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 @@ -15,9 +15,12 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os +import sys  import syslog  import xmltodict +import vyos.opmode +  from argparse import ArgumentParser  from vyos.configquery import CliShellApiConfigQuery  from vyos.configquery import ConfigTreeQuery @@ -31,36 +34,23 @@ conntrackd_bin = '/usr/sbin/conntrackd'  conntrackd_config = '/run/conntrackd/conntrackd.conf'  failover_state_file = '/var/run/vyatta-conntrackd-failover-state' -parser = ArgumentParser(description='Conntrack Sync') -group = parser.add_mutually_exclusive_group() -group.add_argument('--restart', help='Restart connection tracking synchronization service', action='store_true') -group.add_argument('--reset-cache-internal', help='Reset internal cache', action='store_true') -group.add_argument('--reset-cache-external', help='Reset external cache', action='store_true') -group.add_argument('--show-internal', help='Show internal (main) tracking cache', action='store_true') -group.add_argument('--show-external', help='Show external (main) tracking cache', action='store_true') -group.add_argument('--show-internal-expect', help='Show internal (expect) tracking cache', action='store_true') -group.add_argument('--show-external-expect', help='Show external (expect) tracking cache', action='store_true') -group.add_argument('--show-statistics', help='Show connection syncing statistics', action='store_true') -group.add_argument('--show-status', help='Show conntrack-sync status', action='store_true') -  def is_configured():      """ Check if conntrack-sync service is configured """      config = CliShellApiConfigQuery()      if not config.exists(['service', 'conntrack-sync']): -        print('Service conntrackd-sync not configured!') -        exit(1) +        raise vyos.opmode.UnconfiguredSubsystem("conntrack-sync is not configured!")  def send_bulk_update():      """ send bulk update of internal-cache to other systems """      tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -B')      if tmp > 0: -        print('ERROR: failed to send bulk update to other conntrack-sync systems') +        raise vyos.opmode.Error('Failed to send bulk update to other conntrack-sync systems')  def request_sync():      """ request resynchronization with other systems """      tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -n')      if tmp > 0: -        print('ERROR: failed to request resynchronization of external cache') +        raise vyos.opmode.Error('Failed to request resynchronization of external cache')  def flush_cache(direction):      """ flush conntrackd cache (internal or external) """ @@ -68,9 +58,9 @@ def flush_cache(direction):          raise ValueError()      tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -f {direction}')      if tmp > 0: -        print('ERROR: failed to clear {direction} cache') +        raise vyos.opmode.Error('Failed to clear {direction} cache') -def xml_to_stdout(xml): +def from_xml(raw, xml):      out = []      for line in xml.splitlines():          if line == '\n': @@ -78,108 +68,131 @@ def xml_to_stdout(xml):          parsed = xmltodict.parse(line)          out.append(parsed) -    print(render_to_string('conntrackd/conntrackd.op-mode.j2', {'data' : out})) - -if __name__ == '__main__': -    args = parser.parse_args() -    syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID, -                   facility=syslog.LOG_INFO) +    if raw: +        return out +    else: +        return render_to_string('conntrackd/conntrackd.op-mode.j2', {'data' : out}) + +def restart(): +    is_configured() +    if commit_in_progress(): +        raise vyos.opmode.CommitInProgress('Cannot restart conntrackd while a commit is in progress') + +    syslog.syslog('Restarting conntrack sync service...') +    cmd('systemctl restart conntrackd.service') +    # request resynchronization with other systems +    request_sync() +    # send bulk update of internal-cache to other systems +    send_bulk_update() + +def reset_external_cache(): +    is_configured() +    syslog.syslog('Resetting external cache of conntrack sync service...') + +    # flush the external cache +    flush_cache('external') +    # request resynchronization with other systems +    request_sync() + +def reset_internal_cache(): +    is_configured() +    syslog.syslog('Resetting internal cache of conntrack sync service...') +    # flush the internal cache +    flush_cache('internal') + +    # request resynchronization of internal cache with kernel conntrack table +    tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R') +    if tmp > 0: +        print('ERROR: failed to resynchronize internal cache with kernel conntrack table') -    if args.restart: -        is_configured() -        if commit_in_progress(): -            print('Cannot restart conntrackd while a commit is in progress') -            exit(1) - -        syslog.syslog('Restarting conntrack sync service...') -        cmd('systemctl restart conntrackd.service') -        # request resynchronization with other systems -        request_sync() -        # send bulk update of internal-cache to other systems -        send_bulk_update() - -    elif args.reset_cache_external: -        is_configured() -        syslog.syslog('Resetting external cache of conntrack sync service...') +    # send bulk update of internal-cache to other systems +    send_bulk_update() -        # flush the external cache -        flush_cache('external') -        # request resynchronization with other systems -        request_sync() +def _show_cache(raw, opts): +    is_configured() +    out = cmd(f'{conntrackd_bin} -C {conntrackd_config} {opts} -x') +    return from_xml(raw, out) -    elif args.reset_cache_internal: -        is_configured() -        syslog.syslog('Resetting internal cache of conntrack sync service...') -        # flush the internal cache -        flush_cache('internal') +def show_external_cache(raw: bool): +    opts = '-e ct' +    return _show_cache(raw, opts) -        # request resynchronization of internal cache with kernel conntrack table -        tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R') -        if tmp > 0: -            print('ERROR: failed to resynchronize internal cache with kernel conntrack table') +def show_external_expect(raw: bool): +    opts = '-e expect' +    return _show_cache(raw, opts) -        # send bulk update of internal-cache to other systems -        send_bulk_update() +def show_internal_cache(raw: bool): +    opts = '-i ct' +    return _show_cache(raw, opts) -    elif args.show_external or args.show_internal or args.show_external_expect or args.show_internal_expect: -        is_configured() -        opt = '' -        if args.show_external: -            opt = '-e ct' -        elif args.show_external_expect: -            opt = '-e expect' -        elif args.show_internal: -            opt = '-i ct' -        elif args.show_internal_expect: -            opt = '-i expect' - -        if args.show_external or args.show_internal: -            print('Main Table Entries:') -        else: -            print('Expect Table Entries:') -        out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x') -        xml_to_stdout(out) +def show_internal_expect(raw: bool): +    opts = '-i expect' +    return _show_cache(raw, opts) -    elif args.show_statistics: +def show_statistics(raw: bool): +    if raw: +        raise vyos.opmode.UnsupportedOperation("Machine-readable conntrack-sync statistics are not available yet") +    else:          is_configured()          config = ConfigTreeQuery()          print('\nMain Table Statistics:\n') -        call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s') +        call(f'{conntrackd_bin} -C {conntrackd_config} -s')          print()          if config.exists(['service', 'conntrack-sync', 'expect-sync']):              print('\nExpect Table Statistics:\n') -            call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s exp') +            call(f'{conntrackd_bin} -C {conntrackd_config} -s exp')              print() -    elif args.show_status: -        is_configured() -        config = ConfigTreeQuery() -        ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) -        ct_sync_intf = ', '.join(ct_sync_intf) -        failover_state = "no transition yet!" -        expect_sync_protocols = "disabled" - -        if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): -            failover_mechanism = "vrrp" -            vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']) - -        if os.path.isfile(failover_state_file): -            with open(failover_state_file, "r") as f: -                failover_state = f.readline() - -        if config.exists(['service', 'conntrack-sync', 'expect-sync']): -            expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync']) -            if 'all' in expect_sync_protocols: -                expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"] +def show_status(raw: bool): +    is_configured() +    config = ConfigTreeQuery() +    ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) +    ct_sync_intf = ', '.join(ct_sync_intf) +    failover_state = "no transition yet!" +    expect_sync_protocols = [] + +    if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): +        failover_mechanism = "vrrp" +        vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']) + +    if os.path.isfile(failover_state_file): +        with open(failover_state_file, "r") as f: +            failover_state = f.readline() + +    if config.exists(['service', 'conntrack-sync', 'expect-sync']): +        expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync']) +        if 'all' in expect_sync_protocols: +            expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"] + +    if raw: +        status_data = { +            "sync_interface": ct_sync_intf, +            "failover_mechanism": failover_mechanism, +            "sync_group": vrrp_sync_grp, +            "last_transition": failover_state, +            "sync_protocols": expect_sync_protocols +        } + +        return status_data +    else: +        if expect_sync_protocols:              expect_sync_protocols = ', '.join(expect_sync_protocols) - +        else: +            expect_sync_protocols = "disabled"          show_status = (f'\nsync-interface        : {ct_sync_intf}\n'                         f'failover-mechanism    : {failover_mechanism} [sync-group {vrrp_sync_grp}]\n' -                       f'last state transition : {failover_state}' +                       f'last state transition : {failover_state}\n'                         f'ExpectationSync       : {expect_sync_protocols}') -        print(show_status) +        return show_status -    else: -        parser.print_help() -        exit(1) +if __name__ == '__main__': +    syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID, facility=syslog.LOG_INFO) + +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 2cba33cc8..d41a74db3 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -21,6 +21,7 @@ import time  from tabulate import tabulate  from vyos.config import Config +from vyos.template import is_ipv4, is_ipv6  from vyos.util import call  cache_file = r'/run/ddclient/ddclient.cache' @@ -46,7 +47,7 @@ def _get_formatted_host_records(host_data):  def show_status(): -    # A ddclient status file must not always exist +    # A ddclient status file might not always exist      if not os.path.exists(cache_file):          sys.exit(0) @@ -62,9 +63,20 @@ def show_status():              # we pick up the ones we are interested in              for kvraw in line.split(' ')[0].split(','):                  k, v = kvraw.split('=') -                if k in columns.keys(): +                if k in list(columns.keys()) + ['ip', 'status']:  # ip and status are legacy keys                      props[k] = v +            # Extract IPv4 and IPv6 address and status from legacy keys +            # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 +            if 'ip' in props: +                if is_ipv4(props['ip']): +                    props['ipv4'] = props['ip'] +                    props['status-ipv4'] = props['status'] +                elif is_ipv6(props['ip']): +                    props['ipv6'] = props['ip'] +                    props['status-ipv6'] = props['status'] +                del props['ip'] +              # Convert mtime to human readable format              if 'mtime' in props:                  props['mtime'] = time.strftime( diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 7f4fb72e5..db4948d7a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -13,7 +13,6 @@  #  # 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  import typing @@ -24,6 +23,7 @@ from tabulate import tabulate  from vyos.util import convert_data  from vyos.util import seconds_to_human +from vyos.util import cmd  from vyos.configquery import ConfigTreeQuery  import vyos.opmode @@ -46,6 +46,25 @@ def _get_raw_data_sas():      except (vyos.ipsec.ViciInitiateError) as err:          raise vyos.opmode.UnconfiguredSubsystem(err) + +def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str: +    """ +    Template for output for VICI +    Inserts \n after each IKE SA +    :param ra_output_list: IKE SAs list +    :type ra_output_list: list +    :return: formatted string +    :rtype: str +    """ +    output = ''; +    for sa_val in ra_output_list: +        for sa in sa_val.values(): +            swanctl_output: str = cmd( +                f'sudo swanctl -l --ike-id {sa["uniqueid"]}') +        output = f'{output}{swanctl_output}\n\n' +    return output + +  def _get_formatted_output_sas(sas):      sa_data = []      for sa in sas: @@ -444,6 +463,7 @@ def reset_peer(peer: str, tunnel: typing.Optional[str] = None):      except (vyos.ipsec.ViciCommandError) as err:          raise vyos.opmode.IncorrectValue(err) +  def reset_all_peers():      sitetosite_list = _get_all_sitetosite_peers_name_list()      if sitetosite_list: @@ -457,6 +477,7 @@ def reset_all_peers():          raise vyos.opmode.UnconfiguredSubsystem(              'VPN IPSec site-to-site is not configured, aborting') +  def _get_ra_session_list_by_username(username: typing.Optional[str] = None):      """      Return list of remote-access IKE_SAs uniqueids @@ -466,15 +487,15 @@ def _get_ra_session_list_by_username(username: typing.Optional[str] = None):      :rtype:      """      list_sa_id = [] -    sa_list = vyos.ipsec.get_vici_sas() +    sa_list = _get_raw_data_sas()      for sa_val in sa_list:          for sa in sa_val.values():              if 'remote-eap-id' in sa:                  if username: -                    if username == sa['remote-eap-id'].decode(): -                        list_sa_id.append(sa['uniqueid'].decode()) +                    if username == sa['remote-eap-id']: +                        list_sa_id.append(sa['uniqueid'])                  else: -                    list_sa_id.append(sa['uniqueid'].decode()) +                    list_sa_id.append(sa['uniqueid'])      return list_sa_id @@ -556,6 +577,24 @@ def show_sa(raw: bool):      return _get_formatted_output_sas(sa_data) +def _get_output_sas_detail(ra_output_list: list) -> str: +    """ +    Formate all IKE SAs detail output +    :param ra_output_list: IKE SAs list +    :type ra_output_list: list +    :return: formatted RA IKE SAs detail output +    :rtype: str +    """ +    return _get_output_swanctl_sas_from_list(ra_output_list) + + +def show_sa_detail(raw: bool): +    sa_data = _get_raw_data_sas() +    if raw: +        return sa_data +    return _get_output_sas_detail(sa_data) + +  def show_connections(raw: bool):      list_conns = _get_convert_data_connections()      list_sas = _get_raw_data_sas() @@ -573,6 +612,173 @@ def show_connections_summary(raw: bool):          return _get_raw_connections_summary(list_conns, list_sas) +def _get_ra_sessions(username: typing.Optional[str] = None) -> list: +    """ +    Return list of remote-access IKE_SAs from VICI by username. +    If username unspecified, return all remote-access IKE_SAs +    :param username: Username of RA connection +    :type username: str +    :return: list of ra remote-access IKE_SAs +    :rtype: list +    """ +    list_sa = [] +    sa_list = _get_raw_data_sas() +    for conn in sa_list: +        for sa in conn.values(): +            if 'remote-eap-id' in sa: +                if username: +                    if username == sa['remote-eap-id']: +                        list_sa.append(conn) +                else: +                    list_sa.append(conn) +    return list_sa + + +def _filter_ikesas(list_sa: list, filter_key: str, filter_value: str) -> list: +    """ +    Filter IKE SAs by specifice key +    :param list_sa: list of IKE SAs +    :type list_sa: list +    :param filter_key: Filter Key +    :type filter_key: str +    :param filter_value: Filter Value +    :type filter_value: str +    :return: Filtered list of IKE SAs +    :rtype: list +    """ +    filtered_sa_list = [] +    for conn in list_sa: +        for sa in conn.values(): +            if sa[filter_key] and sa[filter_key] == filter_value: +                filtered_sa_list.append(conn) +    return filtered_sa_list + + +def _get_last_installed_childsa(sa: dict) -> str: +    """ +    Return name of last installed active Child SA +    :param sa: Dictionary with Child SAs +    :type sa: dict +    :return: Name of the Last installed active Child SA +    :rtype: str +    """ +    child_sa_name = None +    child_sa_id = 0 +    for sa_name, child_sa in sa['child-sas'].items(): +        if child_sa['state'] == 'INSTALLED': +            if child_sa_id == 0 or int(child_sa['uniqueid']) > child_sa_id: +                child_sa_id = int(child_sa['uniqueid']) +                child_sa_name = sa_name +    return child_sa_name + + +def _get_formatted_ike_proposal(sa: dict) -> str: +    """ +    Return IKE proposal string in format +    EncrALG-EncrKeySize/PFR/HASH/DH-GROUP +    :param sa: IKE SA +    :type sa: dict +    :return: IKE proposal string +    :rtype: str +    """ +    proposal = '' +    proposal = f'{proposal}{sa["encr-alg"]}' if 'encr-alg' in sa else proposal +    proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal +    proposal = f'{proposal}/{sa["prf-alg"]}' if 'prf-alg' in sa else proposal +    proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal +    proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal +    return proposal + + +def _get_formatted_ipsec_proposal(sa: dict) -> str: +    """ +    Return IPSec proposal string in format +    Protocol: EncrALG-EncrKeySize/HASH/PFS +    :param sa: Child SA +    :type sa: dict +    :return: IPSec proposal string +    :rtype: str +    """ +    proposal = '' +    proposal = f'{proposal}{sa["protocol"]}' if 'protocol' in sa else proposal +    proposal = f'{proposal}:{sa["encr-alg"]}' if 'encr-alg' in sa else proposal +    proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal +    proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal +    proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal +    return proposal + + +def _get_output_ra_sas_detail(ra_output_list: list) -> str: +    """ +    Formate RA IKE SAs detail output +    :param ra_output_list: IKE SAs list +    :type ra_output_list: list +    :return: formatted RA IKE SAs detail output +    :rtype: str +    """ +    return _get_output_swanctl_sas_from_list(ra_output_list) + + +def _get_formatted_output_ra_summary(ra_output_list: list): +    sa_data = [] +    for conn in ra_output_list: +        for sa in conn.values(): +            sa_id = sa['uniqueid'] if 'uniqueid' in sa else '' +            sa_username = sa['remote-eap-id'] if 'remote-eap-id' in sa else '' +            sa_protocol = f'IKEv{sa["version"]}' if 'version' in sa else '' +            sa_remotehost = sa['remote-host'] if 'remote-host' in sa else '' +            sa_remoteid = sa['remote-id'] if 'remote-id' in sa else '' +            sa_ike_proposal = _get_formatted_ike_proposal(sa) +            sa_tunnel_ip = sa['remote-vips'] +            child_sa_key = _get_last_installed_childsa(sa) +            if child_sa_key: +                child_sa = sa['child-sas'][child_sa_key] +                sa_ipsec_proposal = _get_formatted_ipsec_proposal(child_sa) +                sa_state = "UP" +                sa_uptime = seconds_to_human(sa['established']) +            else: +                sa_ipsec_proposal = '' +                sa_state = "DOWN" +                sa_uptime = '' +            sa_data.append( +                [sa_id, sa_username, sa_protocol, sa_state, sa_uptime, +                 sa_tunnel_ip, +                 sa_remotehost, sa_remoteid, sa_ike_proposal, +                 sa_ipsec_proposal]) + +    headers = ["Connection ID", "Username", "Protocol", "State", "Uptime", +               "Tunnel IP", "Remote Host", "Remote ID", "IKE Proposal", +               "IPSec Proposal"] +    sa_data = sorted(sa_data, key=_alphanum_key) +    output = tabulate(sa_data, headers) +    return output + + +def show_ra_detail(raw: bool, username: typing.Optional[str] = None, +                   conn_id: typing.Optional[str] = None): +    list_sa: list = _get_ra_sessions() +    if username: +        list_sa = _filter_ikesas(list_sa, 'remote-eap-id', username) +    elif conn_id: +        list_sa = _filter_ikesas(list_sa, 'uniqueid', conn_id) +    if not list_sa: +        raise vyos.opmode.IncorrectValue( +            f'No active connections found, aborting') +    if raw: +        return list_sa +    return _get_output_ra_sas_detail(list_sa) + + +def show_ra_summary(raw: bool): +    list_sa: list = _get_ra_sessions() +    if not list_sa: +        raise vyos.opmode.IncorrectValue( +            f'No active connections found, aborting') +    if raw: +        return list_sa +    return _get_formatted_output_ra_summary(list_sa) + +  if __name__ == '__main__':      try:          res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py deleted file mode 100755 index 73688c4ea..000000000 --- a/src/op_mode/show_vpn_ra.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program.  If not, see <http://www.gnu.org/licenses/>. - -import os -import sys -import re - -from vyos.util import popen - -# chech connection to pptp and l2tp daemon -def get_sessions(): -    absent_pptp = False -    absent_l2tp = False -    pptp_cmd = "accel-cmd -p 2003 show sessions" -    l2tp_cmd = "accel-cmd -p 2004 show sessions" -    err_pattern = "^Connection.+failed$" -    # This value for chack only output header without sessions. -    len_def_header = 170 -     -    # Check pptp -    output, err = popen(pptp_cmd, decode='utf-8') -    if not err and len(output) > len_def_header and not re.search(err_pattern, output): -        print(output) -    else: -        absent_pptp = True - -    # Check l2tp -    output, err = popen(l2tp_cmd, decode='utf-8') -    if not err and len(output) > len_def_header and not re.search(err_pattern, output): -        print(output) -    else: -        absent_l2tp = True - -    if absent_l2tp and absent_pptp: -        print("No active remote access VPN sessions") - - -def main(): -    get_sessions() - - -if __name__ == '__main__': -    main() diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 529b5bd0f..eb601a456 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -17,6 +17,7 @@  import argparse  from sys import exit +from vyos.configquery import ConfigTreeQuery  from vyos.util import cmd  parser = argparse.ArgumentParser() @@ -49,6 +50,11 @@ def qmi_cmd(device, command, silent=False):  if __name__ == '__main__':      args = parser.parse_args() +    tmp = ConfigTreeQuery() +    if not tmp.exists(['interfaces', 'wwan', args.interface]): +        print(f'Interface "{args.interface}" unconfigured!') +        exit(1) +      # remove the WWAN prefix from the interface, required for the CDC interface      if_num = args.interface.replace('wwan','')      cdc = f'/dev/cdc-wdm{if_num}' diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cd73f38ec..acaa383b4 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -283,7 +283,7 @@ class MultipartRequest(Request):          return self._headers      async def form(self) -> FormData: -        if not hasattr(self, "_form"): +        if self._form is None:              assert (                  parse_options_header is not None              ), "The `python-multipart` library must be installed to use form parsing." diff --git a/src/systemd/vyos-wan-load-balance.service b/src/systemd/vyos-wan-load-balance.service new file mode 100644 index 000000000..7d62a2ff6 --- /dev/null +++ b/src/systemd/vyos-wan-load-balance.service @@ -0,0 +1,15 @@ +[Unit] +Description=VyOS WAN load-balancing service +After=vyos-router.service + +[Service] +ExecStart=/opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid +ExecReload=/bin/kill -s SIGTERM $MAINPID && sleep 5 && /opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid +ExecStop=/bin/kill -s SIGTERM $MAINPID +PIDFile=/var/run/vyatta/wlb.pid +KillMode=process +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target | 
