diff options
Diffstat (limited to 'src')
61 files changed, 1288 insertions, 189 deletions
| diff --git a/src/completion/list_bgp_neighbors.sh b/src/completion/list_bgp_neighbors.sh index f74f102ef..869a7ab0a 100755 --- a/src/completion/list_bgp_neighbors.sh +++ b/src/completion/list_bgp_neighbors.sh @@ -1,5 +1,5 @@  #!/bin/sh -# 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 @@ -18,19 +18,21 @@  ipv4=0  ipv6=0 +vrf=""  while [[ "$#" -gt 0 ]]; do      case $1 in          -4|--ipv4) ipv4=1 ;;          -6|--ipv6) ipv6=1 ;;          -b|--both) ipv4=1; ipv6=1 ;; +        --vrf) vrf="vrf name $2"; shift ;;          *) echo "Unknown parameter passed: $1" ;;      esac      shift  done  declare -a vals -eval "vals=($(cli-shell-api listActiveNodes protocols bgp neighbor))" +eval "vals=($(cli-shell-api listActiveNodes $vrf protocols bgp neighbor))"  if [ $ipv4 -eq 1 ] && [ $ipv6 -eq 1 ]; then      echo -n '<x.x.x.x>' '<h:h:h:h:h:h:h:h>' ${vals[@]} @@ -54,9 +56,10 @@ elif [ $ipv6 -eq 1 ] ; then       done  else      echo "Usage:" -    echo "-4|--ipv4    list only IPv4 peers" -    echo "-6|--ipv6    list only IPv6 peers" -    echo "--both       list both IP4 and IPv6 peers" +    echo "-4|--ipv4      list only IPv4 peers" +    echo "-6|--ipv6      list only IPv6 peers" +    echo "--both         list both IP4 and IPv6 peers" +    echo "--vrf <name>   apply command to given VRF (optional)"      echo ""      exit 1  fi diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 2110fd9e0..ac3dc536b 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -90,10 +90,10 @@ def get_config(config=None):              container['name'][name] = dict_merge(default_values, container['name'][name])      # Delete container network, delete containers -    tmp = node_changed(conf, base + ['container', 'network']) +    tmp = node_changed(conf, base + ['network'])      if tmp: container.update({'network_remove' : tmp}) -    tmp = node_changed(conf, base + ['container', 'name']) +    tmp = node_changed(conf, base + ['name'])      if tmp: container.update({'container_remove' : tmp})      return container @@ -132,7 +132,7 @@ def verify(container):                  # Check if the specified container network exists                  network_name = list(container_config['network'])[0] -                if network_name not in container['network']: +                if network_name not in container.get('network', {}):                      raise ConfigError(f'Container network "{network_name}" does not exist!')                  if 'address' in container_config['network'][network_name]: @@ -270,12 +270,13 @@ def apply(container):      # Option "--force" allows to delete containers with any status      if 'container_remove' in container:          for name in container['container_remove']: -            call(f'podman stop {name}') +            call(f'podman stop --time 3 {name}')              call(f'podman rm --force {name}')      # Delete old networks if needed      if 'network_remove' in container:          for network in container['network_remove']: +            call(f'podman network rm {network}')              tmp = f'/etc/cni/net.d/{network}.conflist'              if os.path.exists(tmp):                  os.unlink(tmp) @@ -294,7 +295,7 @@ def apply(container):                  # check if there is a container by that name running                  tmp = _cmd('podman ps -a --format "{{.Names}}"')                  if name in tmp: -                    _cmd(f'podman stop {name}') +                    _cmd(f'podman stop --time 3 {name}')                      _cmd(f'podman rm --force {name}')                  continue diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index f1c2d1f43..d0d87d73e 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -98,6 +98,9 @@ def get_config(config=None):                              dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node))                              continue +                        if subnode == 'any': +                            subnode = '*' +                          for address in rdata['address']:                              zone['records'].append({                                  'name': subnode, @@ -263,6 +266,12 @@ def verify(dns):              if 'server' not in dns['domain'][domain]:                  raise ConfigError(f'No server configured for domain {domain}!') +    if 'dns64_prefix' in dns: +        dns_prefix = dns['dns64_prefix'].split('/')[1] +        # RFC 6147 requires prefix /96 +        if int(dns_prefix) != 96: +            raise ConfigError('DNS 6to4 prefix must be of length /96') +      if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']:          for error in dns['authoritative_zone_errors']:              print(error) diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py index 9a5d278e9..ab1c69259 100755 --- a/src/conf_mode/firewall-interface.py +++ b/src/conf_mode/firewall-interface.py @@ -64,6 +64,11 @@ def get_config(config=None):      return if_firewall +def verify_chain(table, chain): +    # Verify firewall applied +    code = run(f'nft list chain {table} {chain}') +    return code == 0 +  def verify(if_firewall):      # bail out early - looks like removal from running config      if not if_firewall: @@ -80,6 +85,9 @@ def verify(if_firewall):                  if name not in if_firewall['firewall']['name']:                      raise ConfigError(f'Invalid firewall name "{name}"') +                if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'): +                    raise ConfigError('Firewall did not apply') +              if 'ipv6_name' in if_firewall[direction]:                  name = if_firewall[direction]['ipv6_name'] @@ -89,6 +97,9 @@ def verify(if_firewall):                  if name not in if_firewall['firewall']['ipv6_name']:                      raise ConfigError(f'Invalid firewall ipv6-name "{name}"') +                if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'): +                    raise ConfigError('Firewall did not apply') +      return None  def generate(if_firewall): diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 6924bf555..07eca722f 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.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 @@ -26,9 +26,17 @@ 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.firewall import geoip_update +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements  from vyos.template import render +from vyos.util import call  from vyos.util import cmd  from vyos.util import dict_search_args +from vyos.util import dict_search_recursive  from vyos.util import process_named_running  from vyos.util import run  from vyos.xml import defaults @@ -79,10 +87,26 @@ nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']  valid_groups = [      'address_group', +    'domain_group',      'network_group',      'port_group'  ] +nested_group_types = [ +    'address_group', 'network_group', 'mac_group', +    'port_group', 'ipv6_address_group', 'ipv6_network_group' +] + +group_set_prefix = { +    'A_': 'address_group', +    'A6_': 'ipv6_address_group', +    'D_': 'domain_group', +    'M_': 'mac_group', +    'N_': 'network_group', +    'N6_': 'ipv6_network_group', +    'P_': 'port_group' +} +  snmp_change_type = {      'unknown': 0,      'add': 1, @@ -138,6 +162,40 @@ def get_firewall_zones(conf):      return {'name': used_v4, 'ipv6_name': used_v6} +def geoip_updated(conf, firewall): +    diff = get_config_diff(conf) +    node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) + +    out = { +        'name': [], +        'ipv6_name': [], +        'deleted_name': [], +        'deleted_ipv6_name': [] +    } +    updated = False + +    for key, path in dict_search_recursive(firewall, 'geoip'): +        set_name = f'GEOIP_CC_{path[1]}_{path[3]}' +        if path[0] == 'name': +            out['name'].append(set_name) +        elif path[0] == 'ipv6_name': +            out['ipv6_name'].append(set_name) +        updated = True + +    if 'delete' in node_diff: +        for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): +            set_name = f'GEOIP_CC_{path[1]}_{path[3]}' +            if path[0] == 'name': +                out['deleted_name'].append(set_name) +            elif path[0] == 'ipv6-name': +                out['deleted_ipv6_name'].append(set_name) +            updated = True + +    if updated: +        return out + +    return False +  def get_config(config=None):      if config:          conf = config @@ -162,6 +220,8 @@ def get_config(config=None):                                          key_mangling=('-', '_'), get_first_key=True,                                          no_tag_node_value_mangle=True) +    firewall['geoip_updated'] = geoip_updated(conf, firewall) +      return firewall  def verify_rule(firewall, rule_conf, ipv6): @@ -207,6 +267,16 @@ def verify_rule(firewall, rule_conf, ipv6):          if side in rule_conf:              side_conf = rule_conf[side] +            if dict_search_args(side_conf, 'geoip', 'country_code'): +                if 'address' in side_conf: +                    raise ConfigError('Address and GeoIP cannot both be defined') + +                if dict_search_args(side_conf, 'group', 'address_group'): +                    raise ConfigError('Address-group and GeoIP cannot both be defined') + +                if dict_search_args(side_conf, 'group', 'network_group'): +                    raise ConfigError('Network-group and GeoIP cannot both be defined') +              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') @@ -235,11 +305,34 @@ def verify_rule(firewall, rule_conf, ipv6):                  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_nested_group(group_name, group, groups, seen): +    if 'include' not in group: +        return + +    for g in group['include']: +        if g not in groups: +            raise ConfigError(f'Nested group "{g}" does not exist') + +        if g in seen: +            raise ConfigError(f'Group "{group_name}" has a circular reference') +             +        seen.append(g) + +        if 'include' in groups[g]: +            verify_nested_group(g, groups[g], groups, seen) +  def verify(firewall):      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') +    if 'group' in firewall: +        for group_type in nested_group_types: +            if group_type in firewall['group']: +                groups = firewall['group'][group_type] +                for group_name, group in groups.items(): +                    verify_nested_group(group_name, group, groups, []) +      for name in ['name', 'ipv6_name']:          if name in firewall:              for name_id, name_conf in firewall[name].items(): @@ -271,55 +364,75 @@ def verify(firewall):      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 = [] +    commands_chains = [] +    commands_sets = []      for table in ['ip filter', 'ip6 filter']: +        name_node = 'name' if table == 'ip filter' else 'ipv6_name' +        chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX          state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6' -        json_str = cmd(f'nft -j list table {table}') +        iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains + +        geoip_list = [] +        if firewall['geoip_updated']: +            geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' +            geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] +  +        json_str = cmd(f'nft -t -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 chain in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']: -                    if 'state_policy' not in firewall: -                        commands.append(f'delete chain {table} {chain}') -                    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.replace(NAME_PREFIX, "", 1)) != None: -                        commands.append(f'flush chain {table} {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: +                if chain in preserve_chains or chain.startswith("VZONE"): +                    continue + +                if chain == state_chain: +                    command = 'delete' if 'state_policy' not in firewall else 'flush' +                    commands_chains.append(f'{command} chain {table} {chain}') +                elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None: +                    commands.append(f'flush chain {table} {chain}') +                else: +                    commands_chains.append(f'delete chain {table} {chain}') + +            if 'rule' in item:                  rule = item['rule'] -                if rule['chain'] in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: -                    if 'expr' in rule and any([True for expr in rule['expr'] if dict_search_args(expr, 'jump', 'target') == state_chain]): -                        if 'state_policy' not in firewall: -                            chain = rule['chain'] -                            handle = rule['handle'] +                chain = rule['chain'] +                handle = rule['handle'] + +                if chain in iface_chains: +                    target, _ = next(dict_search_recursive(rule['expr'], 'target')) + +                    if target == state_chain and 'state_policy' not in firewall: +                        commands.append(f'delete rule {table} {chain} handle {handle}') + +                    if target.startswith(chain_prefix): +                        if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None:                              commands.append(f'delete rule {table} {chain} handle {handle}') -            elif 'set' in item: + +            if 'set' in item:                  set_name = item['set']['name'] -                commands_end.append(f'delete set {table} {set_name}') -    return commands + commands_end + +                if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: +                    commands_sets.append(f'delete set {table} {set_name}') +                    continue +                 +                if set_name.startswith("RECENT_"): +                    commands_sets.append(f'delete set {table} {set_name}') +                    continue + +                for prefix, group_type in group_set_prefix.items(): +                    if set_name.startswith(prefix): +                        group_name = set_name.replace(prefix, "", 1) +                        if dict_search_args(firewall, 'group', group_type, group_name) != None: +                            commands_sets.append(f'flush set {table} {set_name}') +                        else: +                            commands_sets.append(f'delete set {table} {set_name}') +    return commands + commands_chains + commands_sets  def generate(firewall):      if not os.path.exists(nftables_conf): @@ -328,7 +441,6 @@ def generate(firewall):          firewall['cleanup_commands'] = cleanup_commands(firewall)      render(nftables_conf, 'firewall/nftables.j2', firewall) -    render(nftables_defines_conf, 'firewall/nftables-defines.j2', firewall)      return None  def apply_sysfs(firewall): @@ -408,6 +520,27 @@ def apply(firewall):      if install_result == 1:          raise ConfigError('Failed to apply firewall') +    # set fireall group domain-group xxx +    if 'group' in firewall: +        if 'domain_group' in firewall['group']: +            # T970 Enable a resolver (systemd daemon) that checks +            # domain-group addresses and update entries for domains by timeout +            # If router loaded without internet connection or for synchronization +            call('systemctl restart vyos-domain-group-resolve.service') +            for group, group_config in firewall['group']['domain_group'].items(): +                domains = [] +                if group_config.get('address') is not None: +                    for address in group_config.get('address'): +                        domains.append(address) +                # Add elements to domain-group, try to resolve domain => ip +                # and add elements to nft set +                ip_dict = get_ips_domains_dict(domains) +                elements = sum(ip_dict.values(), []) +                nft_init_set(f'D_{group}') +                nft_add_set_elements(f'D_{group}', elements) +        else: +            call('systemctl stop vyos-domain-group-resolve.service') +      if 'state_policy' in firewall and not state_policy_rule_exists():          for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:              cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') @@ -420,6 +553,12 @@ def apply(firewall):      if firewall['policy_resync']:          resync_policy_route() +    if firewall['geoip_updated']: +        # Call helper script to Update set contents +        if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: +            print('Updating GeoIP. Please wait...') +            geoip_update(firewall) +      post_apply_trap(firewall)      return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 4167594e3..7e146f446 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -36,6 +36,7 @@ from vyos.ifconfig import BondIf  from vyos.ifconfig import Section  from vyos.util import dict_search  from vyos.validate import has_address_configured +from vyos.validate import has_vrf_configured  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -109,20 +110,26 @@ def get_config(config=None):          for interface, interface_config in bond['member']['interface'].items():              # Check if member interface is already member of another bridge              tmp = is_member(conf, interface, 'bridge') -            if tmp: interface_config.update({'is_bridge_member' : tmp}) +            if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})              # Check if member interface is already member of a bond              tmp = is_member(conf, interface, 'bonding') -            if tmp and bond['ifname'] not in tmp: -                interface_config.update({'is_bond_member' : tmp}) +            for tmp in is_member(conf, interface, 'bonding'): +                if bond['ifname'] == tmp: +                    continue +                bond['member']['interface'][interface].update({'is_bond_member' : tmp})              # Check if member interface is used as source-interface on another interface              tmp = is_source_interface(conf, interface) -            if tmp: interface_config.update({'is_source_interface' : tmp}) +            if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})              # bond members must not have an assigned address              tmp = has_address_configured(conf, interface) -            if tmp: interface_config.update({'has_address' : ''}) +            if tmp: bond['member']['interface'][interface].update({'has_address' : {}}) + +            # bond members must not have a VRF attached +            tmp = has_vrf_configured(conf, interface) +            if tmp: bond['member']['interface'][interface].update({'has_vrf' : {}})      return bond @@ -167,11 +174,11 @@ def verify(bond):                  raise ConfigError(error_msg + 'it does not exist!')              if 'is_bridge_member' in interface_config: -                tmp = next(iter(interface_config['is_bridge_member'])) +                tmp = interface_config['is_bridge_member']                  raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')              if 'is_bond_member' in interface_config: -                tmp = next(iter(interface_config['is_bond_member'])) +                tmp = interface_config['is_bond_member']                  raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')              if 'is_source_interface' in interface_config: @@ -181,6 +188,8 @@ def verify(bond):              if 'has_address' in interface_config:                  raise ConfigError(error_msg + 'it has an address assigned!') +            if 'has_vrf' in interface_config: +                raise ConfigError(error_msg + 'it has a VRF assigned!')      if 'primary' in bond:          if bond['primary'] not in bond['member']['interface']: diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 38ae727c1..cd0d9003b 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -31,6 +31,7 @@ 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 +from vyos.validate import has_vrf_configured  from vyos.xml import defaults  from vyos.util import cmd @@ -93,6 +94,10 @@ def get_config(config=None):              tmp = has_address_configured(conf, interface)              if tmp: bridge['member']['interface'][interface].update({'has_address' : ''}) +            # Bridge members must not have a VRF attached +            tmp = has_vrf_configured(conf, interface) +            if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''}) +              # VLAN-aware bridge members must not have VLAN interface configuration              tmp = has_vlan_subinterface_configured(conf,interface)              if 'enable_vlan' in bridge and tmp: @@ -118,11 +123,11 @@ def verify(bridge):                  raise ConfigError('Loopback interface "lo" can not be added to a bridge')              if 'is_bridge_member' in interface_config: -                tmp = next(iter(interface_config['is_bridge_member'])) +                tmp = interface_config['is_bridge_member']                  raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')              if 'is_bond_member' in interface_config: -                tmp = next(iter(interface_config['is_bond_member'])) +                tmp = interface_config['is_bond_member']                  raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')              if 'is_source_interface' in interface_config: @@ -132,9 +137,12 @@ def verify(bridge):              if 'has_address' in interface_config:                  raise ConfigError(error_msg + 'it has an address assigned!') +            if 'has_vrf' in interface_config: +                raise ConfigError(error_msg + 'it has a VRF assigned!') +              if 'enable_vlan' in bridge:                  if 'has_vlan' in interface_config: -                    raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') +                    raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')                  if 'wlan' in interface:                      raise ConfigError(error_msg + 'VLAN aware cannot be set!') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index fec4456fb..30e7a2af7 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -31,6 +31,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ethtool import Ethtool  from vyos.ifconfig import EthernetIf  from vyos.pki import find_chain @@ -83,6 +84,7 @@ def verify(ethernet):      verify_dhcpv6(ethernet)      verify_address(ethernet)      verify_vrf(ethernet) +    verify_bond_bridge_member(ethernet)      verify_eapol(ethernet)      verify_mirror_redirect(ethernet) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index b9cf2fa3c..08cc3a48d 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -27,6 +27,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ifconfig import GeneveIf  from vyos import ConfigError @@ -64,6 +65,7 @@ def verify(geneve):      verify_mtu_ipv6(geneve)      verify_address(geneve) +    verify_bond_bridge_member(geneve)      verify_mirror_redirect(geneve)      if 'remote' not in geneve: diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 6a486f969..ca321e01d 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -26,6 +26,7 @@ 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_bond_bridge_member  from vyos.ifconfig import L2TPv3If  from vyos.util import check_kmod  from vyos.validate import is_addr_assigned @@ -77,6 +78,7 @@ def verify(l2tpv3):      verify_mtu_ipv6(l2tpv3)      verify_address(l2tpv3) +    verify_bond_bridge_member(l2tpv3)      verify_mirror_redirect(l2tpv3)      return None diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 279dd119b..03a010086 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.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 @@ -21,16 +21,20 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_interface_dict -from vyos.ifconfig import MACsecIf -from vyos.ifconfig import Interface -from vyos.template import render -from vyos.util import call +from vyos.configdict import is_node_changed  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.configverify import verify_bond_bridge_member +from vyos.ifconfig import MACsecIf +from vyos.ifconfig import Interface +from vyos.template import render +from vyos.util import call +from vyos.util import dict_search +from vyos.util import is_systemd_service_running  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -55,6 +59,12 @@ def get_config(config=None):          source_interface = conf.return_effective_value(['source-interface'])          macsec.update({'source_interface': source_interface}) +    if is_node_changed(conf, base + [ifname, 'security']): +        macsec.update({'shutdown_required': {}}) + +    if is_node_changed(conf, base + [ifname, 'source_interface']): +        macsec.update({'shutdown_required': {}}) +      return macsec @@ -67,22 +77,15 @@ def verify(macsec):      verify_vrf(macsec)      verify_mtu_ipv6(macsec)      verify_address(macsec) +    verify_bond_bridge_member(macsec)      verify_mirror_redirect(macsec) -    if not (('security' in macsec) and -            ('cipher' in macsec['security'])): -        raise ConfigError( -            'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) +    if dict_search('security.cipher', macsec) == None: +        raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) -    if (('security' in macsec) and -        ('encrypt' in macsec['security'])): -        tmp = macsec.get('security') - -        if not (('mka' in tmp) and -                ('cak' in tmp['mka']) and -                ('ckn' in tmp['mka'])): -            raise ConfigError('Missing mandatory MACsec security ' -                              'keys as encryption is enabled!') +    if dict_search('security.encrypt', macsec) != None: +        if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: +            raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')      if 'source_interface' in macsec:          # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -97,33 +100,35 @@ def verify(macsec):  def generate(macsec): -    render(wpa_suppl_conf.format(**macsec), -           'macsec/wpa_supplicant.conf.j2', macsec) +    render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)      return None  def apply(macsec): -    # Remove macsec interface -    if 'deleted' in macsec: -        call('systemctl stop wpa_supplicant-macsec@{source_interface}' -             .format(**macsec)) +    systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec) + +    # Remove macsec interface on deletion or mandatory parameter change +    if 'deleted' in macsec or 'shutdown_required' in macsec: +        call(f'systemctl stop {systemd_service}')          if macsec['ifname'] in interfaces():              tmp = MACsecIf(macsec['ifname'])              tmp.remove() -        # delete configuration on interface removal -        if os.path.isfile(wpa_suppl_conf.format(**macsec)): -            os.unlink(wpa_suppl_conf.format(**macsec)) +        if 'deleted' in macsec: +            # delete configuration on interface removal +            if os.path.isfile(wpa_suppl_conf.format(**macsec)): +                os.unlink(wpa_suppl_conf.format(**macsec)) -    else: -        # It is safe to "re-create" the interface always, there is a sanity -        # check that the interface will only be create if its non existent -        i = MACsecIf(**macsec) -        i.update(macsec) +            return None + +    # It is safe to "re-create" the interface always, there is a sanity +    # check that the interface will only be create if its non existent +    i = MACsecIf(**macsec) +    i.update(macsec) -        call('systemctl restart wpa_supplicant-macsec@{source_interface}' -             .format(**macsec)) +    if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: +        call(f'systemctl reload-or-restart {systemd_service}')      return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4750ca3e8..ef745d737 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -36,9 +36,12 @@ from vyos.configdict import is_node_changed  from vyos.configverify import verify_vrf  from vyos.configverify import verify_bridge_delete  from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_bond_bridge_member  from vyos.ifconfig import VTunIf  from vyos.pki import load_dh_parameters  from vyos.pki import load_private_key +from vyos.pki import sort_ca_chain +from vyos.pki import verify_ca_chain  from vyos.pki import wrap_certificate  from vyos.pki import wrap_crl  from vyos.pki import wrap_dh_parameters @@ -148,8 +151,14 @@ def verify_pki(openvpn):          if 'ca_certificate' not in tls:              raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') -        if tls['ca_certificate'] not in pki['ca']: -            raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') +        for ca_name in tls['ca_certificate']: +            if ca_name not in pki['ca']: +                raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + +        if len(tls['ca_certificate']) > 1: +            sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) +            if not verify_ca_chain(sorted_chain, pki['ca']): +                raise ConfigError(f'CA certificates are not a valid chain')          if mode != 'client' and 'auth_key' not in tls:              if 'certificate' not in tls: @@ -495,6 +504,7 @@ def verify(openvpn):              raise ConfigError('Username for authentication is missing')      verify_vrf(openvpn) +    verify_bond_bridge_member(openvpn)      verify_mirror_redirect(openvpn)      return None @@ -516,21 +526,28 @@ def generate_pki_files(openvpn):      if tls:          if 'ca_certificate' in tls: -            cert_name = tls['ca_certificate'] -            pki_ca = pki['ca'][cert_name] +            cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') +            crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') -            if 'certificate' in pki_ca: -                cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') -                write_file(cert_path, wrap_certificate(pki_ca['certificate']), -                           user=user, group=group, mode=0o600) +            if os.path.exists(cert_path): +                os.unlink(cert_path) + +            if os.path.exists(crl_path): +                os.unlink(crl_path) + +            for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): +                pki_ca = pki['ca'][cert_name] + +                if 'certificate' in pki_ca: +                    write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", +                               user=user, group=group, mode=0o600, append=True) -            if 'crl' in pki_ca: -                for crl in pki_ca['crl']: -                    crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') -                    write_file(crl_path, wrap_crl(crl), user=user, group=group, -                               mode=0o600) +                if 'crl' in pki_ca: +                    for crl in pki_ca['crl']: +                        write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, +                                   mode=0o600, append=True) -                openvpn['tls']['crl'] = True +                    openvpn['tls']['crl'] = True          if 'certificate' in tls:              cert_name = tls['certificate'] diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 1cd3fe276..f26a50a0e 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -26,6 +26,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ifconfig import MACVLANIf  from vyos import ConfigError diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index eff7f373c..acef1fda7 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -29,6 +29,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ifconfig import Interface  from vyos.ifconfig import Section  from vyos.ifconfig import TunnelIf @@ -158,6 +159,7 @@ def verify(tunnel):      verify_mtu_ipv6(tunnel)      verify_address(tunnel)      verify_vrf(tunnel) +    verify_bond_bridge_member(tunnel)      verify_mirror_redirect(tunnel)      if 'source_interface' in tunnel: diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index f44d754ba..bf0f6840d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -29,6 +29,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ifconfig import Interface  from vyos.ifconfig import VXLANIf  from vyos.template import is_ipv6 @@ -144,6 +145,7 @@ def verify(vxlan):      verify_mtu_ipv6(vxlan)      verify_address(vxlan) +    verify_bond_bridge_member(vxlan)      verify_mirror_redirect(vxlan)      return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 180ffa507..61bab2feb 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -29,6 +29,7 @@ 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_bond_bridge_member  from vyos.ifconfig import WireGuardIf  from vyos.util import check_kmod  from vyos.util import check_port_availability @@ -71,6 +72,7 @@ def verify(wireguard):      verify_mtu_ipv6(wireguard)      verify_address(wireguard)      verify_vrf(wireguard) +    verify_bond_bridge_member(wireguard)      verify_mirror_redirect(wireguard)      if 'private_key' not in wireguard: diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index d34297063..dd798b5a2 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -30,6 +30,7 @@ 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.configverify import verify_bond_bridge_member  from vyos.ifconfig import WiFiIf  from vyos.template import render  from vyos.util import call @@ -194,6 +195,7 @@ def verify(wifi):      verify_address(wifi)      verify_vrf(wifi) +    verify_bond_bridge_member(wifi)      verify_mirror_redirect(wifi)      # use common function to verify VLAN configuration diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 0d6ec9ace..5490a794d 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.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 @@ -18,9 +18,11 @@ import os  from vyos.config import Config  from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.configverify import verify_interface_exists  from vyos.util import call +from vyos.util import get_interface_config  from vyos.template import render +from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -49,6 +51,20 @@ def verify(ntp):          raise ConfigError('NTP server not configured')      verify_vrf(ntp) + +    if 'interface' in ntp: +        # If ntpd should listen on a given interface, ensure it exists +        for interface in ntp['interface']: +            verify_interface_exists(interface) + +            # If we run in a VRF, our interface must belong to this VRF, too +            if 'vrf' in ntp: +                tmp = get_interface_config(interface) +                vrf_name = ntp['vrf'] +                if 'master' not in tmp or tmp['master'] != vrf_name: +                    raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ +                                      f'does not belong to this VRF!') +      return None  def generate(ntp): diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py index 1108aebe6..58c5fd93d 100755 --- a/src/conf_mode/policy-route-interface.py +++ b/src/conf_mode/policy-route-interface.py @@ -24,6 +24,7 @@ from vyos.config import Config  from vyos.ifconfig import Section  from vyos.template import render  from vyos.util import cmd +from vyos.util import run  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -47,6 +48,11 @@ def get_config(config=None):      return if_policy +def verify_chain(table, chain): +    # Verify policy route applied +    code = run(f'nft list chain {table} {chain}') +    return code == 0 +  def verify(if_policy):      # bail out early - looks like removal from running config      if not if_policy: @@ -62,6 +68,12 @@ def verify(if_policy):              if route_name not in if_policy['policy'][route]:                  raise ConfigError(f'Invalid policy route name "{name}"') +            nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_' +            nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle' + +            if not verify_chain(nft_table, nft_prefix + route_name): +                raise ConfigError('Policy route did not apply') +      return None  def generate(if_policy): diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 5de341beb..9fddbd2c6 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -25,6 +25,7 @@ from vyos.config import Config  from vyos.template import render  from vyos.util import cmd  from vyos.util import dict_search_args +from vyos.util import dict_search_recursive  from vyos.util import run  from vyos import ConfigError  from vyos import airbag @@ -33,6 +34,9 @@ airbag.enable()  mark_offset = 0x7FFFFFFF  nftables_conf = '/run/nftables_policy.conf' +ROUTE_PREFIX = 'VYOS_PBR_' +ROUTE6_PREFIX = 'VYOS_PBR6_' +  preserve_chains = [      'VYOS_PBR_PREROUTING',      'VYOS_PBR_POSTROUTING', @@ -46,6 +50,16 @@ valid_groups = [      'port_group'  ] +group_set_prefix = { +    'A_': 'address_group', +    'A6_': 'ipv6_address_group', +#    'D_': 'domain_group', +    'M_': 'mac_group', +    'N_': 'network_group', +    'N6_': 'ipv6_network_group', +    'P_': 'port_group' +} +  def get_policy_interfaces(conf):      out = {}      interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, @@ -166,37 +180,55 @@ def verify(policy):      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 = [] +    commands_chains = [] +    commands_sets = []      for table in ['ip mangle', 'ip6 mangle']: -        json_str = cmd(f'nft -j list table {table}') +        route_node = 'route' if table == 'ip mangle' else 'route6' +        chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX + +        json_str = cmd(f'nft -t -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"): +                if chain in preserve_chains or not chain.startswith("VYOS_PBR"):                      continue + +                if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: +                    commands.append(f'flush chain {table} {chain}') +                else: +                    commands_chains.append(f'delete chain {table} {chain}') + +            if 'rule' in item: +                rule = item['rule'] +                chain = rule['chain'] +                handle = rule['handle'] +                  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 +                    continue + +                target, _ = next(dict_search_recursive(rule['expr'], 'target')) + +                if target.startswith(chain_prefix): +                    if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: +                        commands.append(f'delete rule {table} {chain} handle {handle}') + +            if 'set' in item: +                set_name = item['set']['name'] + +                for prefix, group_type in group_set_prefix.items(): +                    if set_name.startswith(prefix): +                        group_name = set_name.replace(prefix, "", 1) +                        if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: +                            commands_sets.append(f'flush set {table} {set_name}') +                        else: +                            commands_sets.append(f'delete set {table} {set_name}') + +    return commands + commands_chains + commands_sets  def generate(policy):      if not os.path.exists(nftables_conf): diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cd46cbcb4..5aa643476 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,6 +19,7 @@ import os  from sys import exit  from sys import argv +from vyos.base import Warning  from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.configverify import verify_prefix_list @@ -100,6 +101,17 @@ def verify_remote_as(peer_config, bgp_config):      return None +def verify_afi(peer_config, bgp_config): +    if 'address_family' in peer_config: +        return True + +    if 'peer_group' in peer_config: +        peer_group_name = peer_config['peer_group'] +        tmp = dict_search(f'peer_group.{peer_group_name}.address_family', bgp_config) +        if tmp: return True + +    return False +  def verify(bgp):      if not bgp or 'deleted' in bgp:          if 'dependent_vrfs' in bgp: @@ -164,6 +176,9 @@ def verify(bgp):                  if not verify_remote_as(peer_config, bgp):                      raise ConfigError(f'Neighbor "{peer}" remote-as must be set!') +                if not verify_afi(peer_config, bgp): +                    Warning(f'BGP neighbor "{peer}" requires address-family!') +                  # Peer-group member cannot override remote-as of peer-group                  if 'peer_group' in peer_config:                      peer_group = peer_config['peer_group'] diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 56939955d..b247ce2ab 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -81,11 +81,6 @@ def verify(nhrp):                  for map_name, map_conf in nhrp_conf['dynamic_map'].items():                      if 'nbma_domain_name' not in map_conf:                          raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}') - -            if 'cisco_authentication' in nhrp_conf: -                if len(nhrp_conf['cisco_authentication']) > 8: -                    raise ConfigError('Maximum length of the secret is 8 characters!') -      return None  def generate(nhrp): diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py new file mode 100755 index 000000000..5440d1056 --- /dev/null +++ b/src/conf_mode/service_event_handler.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import json +from pathlib import Path + +from vyos.config import Config +from vyos.util import call, dict_search +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + +service_name = 'vyos-event-handler' +service_conf = Path(f'/run/{service_name}.conf') + + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() + +    base = ['service', 'event-handler', 'event'] +    config = conf.get_config_dict(base, +                                  get_first_key=True, +                                  no_tag_node_value_mangle=True) + +    return config + + +def verify(config): +    # bail out early - looks like removal from running config +    if not config: +        return None + +    for name, event_config in config.items(): +        if not dict_search('filter.pattern', event_config) or not dict_search( +                'script.path', event_config): +            raise ConfigError( +                'Event-handler: both pattern and script path items are mandatory' +            ) + +        if dict_search('script.environment.message', event_config): +            raise ConfigError( +                'Event-handler: "message" environment variable is reserved for log message text' +            ) + + +def generate(config): +    if not config: +        # Remove old config and return +        service_conf.unlink(missing_ok=True) +        return None + +    # Write configuration file +    conf_json = json.dumps(config, indent=4) +    service_conf.write_text(conf_json) + +    return None + + +def apply(config): +    if config: +        call(f'systemctl restart {service_name}.service') +    else: +        call(f'systemctl stop {service_name}.service') + + +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 559d1bcd5..61f484129 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -53,6 +53,8 @@ default_config_data = {      'radius_nas_ip': '',      'radius_source_address': '',      'radius_shaper_attr': '', +    'radius_shaper_enable': False, +    'radius_shaper_multiplier': '',      'radius_shaper_vendor': '',      'radius_dynamic_author': '',      'thread_cnt': get_half_cpus() @@ -196,6 +198,18 @@ def get_config(config=None):      if conf.exists(['nas-ip-address']):          ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) +    if conf.exists(['rate-limit', 'attribute']): +        ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + +    if conf.exists(['rate-limit', 'enable']): +        ipoe['radius_shaper_enable'] = True + +    if conf.exists(['rate-limit', 'multiplier']): +        ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + +    if conf.exists(['rate-limit', 'vendor']): +        ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) +      if conf.exists(['source-address']):          ipoe['radius_source_address'] = conf.return_value(['source-address']) diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index daf75d740..62f5e1ddf 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -99,10 +99,6 @@ def get_config(config=None):      monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)      monitoring['nft_chains'] = get_nft_filter_chains() -    if 'authentication' in monitoring or \ -       'url' in monitoring: -        monitoring['influxdb_configured'] = True -      # Redefine azure group-metrics 'single-table' and 'table-per-metric'      if 'azure_data_explorer' in monitoring:          if 'single-table' in monitoring['azure_data_explorer']['group_metrics']: @@ -119,6 +115,9 @@ def get_config(config=None):      # Ignore default XML values if config doesn't exists      # Delete key from dict +    if not conf.exists(base + ['influxdb']): +        del monitoring['influxdb'] +      if not conf.exists(base + ['prometheus-client']):          del monitoring['prometheus_client'] @@ -132,14 +131,15 @@ def verify(monitoring):      if not monitoring:          return None -    if 'influxdb_configured' in monitoring: -        if 'authentication' not in monitoring or \ -           'organization' not in monitoring['authentication'] or \ -           'token' not in monitoring['authentication']: -            raise ConfigError(f'Authentication "organization and token" are mandatory!') +    # Verify influxdb +    if 'influxdb' in monitoring: +        if 'authentication' not in monitoring['influxdb'] or \ +           'organization' not in monitoring['influxdb']['authentication'] or \ +           'token' not in monitoring['influxdb']['authentication']: +            raise ConfigError(f'influxdb authentication "organization and token" are mandatory!') -        if 'url' not in monitoring: -            raise ConfigError(f'Monitoring "url" is mandatory!') +        if 'url' not in monitoring['influxdb']: +            raise ConfigError(f'Monitoring influxdb "url" is mandatory!')      # Verify azure-data-explorer      if 'azure_data_explorer' in monitoring: diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 71b758399..ff7caaa84 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.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 @@ -17,7 +17,7 @@  import os  from sys import exit - +from vyos.base import Warning  from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.template import render @@ -79,22 +79,35 @@ def verify(rtradv):      if 'interface' not in rtradv:          return None -    for interface in rtradv['interface']: -        interface = rtradv['interface'][interface] +    for interface, interface_config in rtradv['interface'].items():          if 'prefix' in interface: -            for prefix in interface['prefix']: -                prefix = interface['prefix'][prefix] -                valid_lifetime = prefix['valid_lifetime'] +            for prefix, prefix_config in interface_config['prefix'].items(): +                valid_lifetime = prefix_config['valid_lifetime']                  if valid_lifetime == 'infinity':                      valid_lifetime = 4294967295 -                preferred_lifetime = prefix['preferred_lifetime'] +                preferred_lifetime = prefix_config['preferred_lifetime']                  if preferred_lifetime == 'infinity':                      preferred_lifetime = 4294967295                  if not (int(valid_lifetime) > int(preferred_lifetime)):                      raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') +        if 'name_server_lifetime' in interface_config: +            # man page states: +            # The maximum duration how long the RDNSS entries are used for name +            # resolution. A value of 0 means the nameserver must no longer be +            # used. The value, if not 0, must be at least MaxRtrAdvInterval. To +            # ensure stale RDNSS info gets removed in a timely fashion, this +            # should not be greater than 2*MaxRtrAdvInterval. +            lifetime = int(interface_config['name_server_lifetime']) +            interval_max = int(interface_config['interval']['max']) +            if lifetime > 0: +                if lifetime < int(interval_max): +                    raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!') +                if lifetime > 2* interval_max: +                    Warning(f'RDNSS lifetime should not exceed "{2 * interval_max}" which is two times "interval max"!') +      return None  def generate(rtradv): @@ -105,15 +118,16 @@ def generate(rtradv):      return None  def apply(rtradv): +    systemd_service = 'radvd.service'      if not rtradv:          # bail out early - looks like removal from running config -        call('systemctl stop radvd.service') +        call(f'systemctl stop {systemd_service}')          if os.path.exists(config_file):              os.unlink(config_file)          return None -    call('systemctl restart radvd.service') +    call(f'systemctl reload-or-restart {systemd_service}')      return None diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 05fc3a97a..0c5063ed3 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -64,6 +64,11 @@ def apply(opt):      value = '0' if (tmp != None) else '1'      write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) +    # enable/disable IPv4 directed broadcast forwarding +    tmp = dict_search('disable_directed_broadcast', opt) +    value = '0' if (tmp != None) else '1' +    write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value) +      # configure multipath      tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)      value = '1' if (tmp != None) else '0' diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index c717286ae..3dcbc995c 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -231,7 +231,7 @@ def apply(login):              if tmp: command += f" --home '{tmp}'"              else: command += f" --home '/home/{user}'" -            command += f' --groups frrvty,vyattacfg,sudo,adm,dip,disk {user}' +            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}'              try:                  cmd(command) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index a9d3bbe31..20132456c 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -52,8 +52,6 @@ def get_config(config=None):          {              'global': {                  'log-file': '/var/log/messages', -                'max-size': 262144, -                'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',                  'selectors': '*.notice;local7.debug',                  'max-files': '5',                  'preserver_fqdn': False diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index db53463cf..23e5162ba 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.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 @@ -20,6 +20,7 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_accel_dict +from vyos.configdict import dict_merge  from vyos.configverify import verify_accel_ppp_base_service  from vyos.pki import wrap_certificate  from vyos.pki import wrap_private_key @@ -50,10 +51,10 @@ def get_config(config=None):      # retrieve common dictionary keys      sstp = get_accel_dict(conf, base, sstp_chap_secrets) -      if sstp:          sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), -                                get_first_key=True, no_tag_node_value_mangle=True) +                                           get_first_key=True, +                                           no_tag_node_value_mangle=True)      return sstp @@ -121,7 +122,6 @@ def generate(sstp):      ca_cert_name = sstp['ssl']['ca_certificate']      pki_ca = sstp['pki']['ca'][ca_cert_name] -      write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))      write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))      write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate'])) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 972d0289b..1b4156895 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -113,8 +113,14 @@ 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"]          table_ids = []          for name, config in vrf['name'].items(): +            # Reserved VRF names +            if name in reserved_names: +                raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') +              # table id is mandatory              if 'table' not in config:                  raise ConfigError(f'VRF "{name}" table id is mandatory!') diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index 070a4deea..a52c52706 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -155,7 +155,7 @@ def get_local_from(zone_policy, local_zone_name):  def cleanup_commands():      commands = []      for table in ['ip filter', 'ip6 filter']: -        json_str = cmd(f'nft -j list table {table}') +        json_str = cmd(f'nft -t -j list table {table}')          obj = loads(json_str)          if 'nftables' not in obj:              continue diff --git a/src/etc/cron.d/vyos-geoip b/src/etc/cron.d/vyos-geoip new file mode 100644 index 000000000..9bb38a850 --- /dev/null +++ b/src/etc/cron.d/vyos-geoip @@ -0,0 +1 @@ +30 4 * * 1 root sg vyattacfg "/usr/libexec/vyos/geoip-update.py --force" >/tmp/geoip-update.log 2>&1 diff --git a/src/etc/cron.hourly/vyos-logrotate-hourly b/src/etc/cron.hourly/vyos-logrotate-hourly deleted file mode 100755 index f4f56a9c2..000000000 --- a/src/etc/cron.hourly/vyos-logrotate-hourly +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -test -x /usr/sbin/logrotate || exit 0 -/usr/sbin/logrotate /etc/logrotate.conf diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index e03d3a29c..4feb7e09a 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -27,6 +27,12 @@ net.ipv4.conf.all.arp_announce=2  # Enable packet forwarding for IPv4  net.ipv4.ip_forward=1 +# Enable directed broadcast forwarding feature described in rfc1812#section-5.3.5.2 and rfc2644. +# Note that setting the 'all' entry to 1 doesn't enable directed broadcast forwarding on all interfaces. +# To enable directed broadcast forwarding on an interface, both the 'all' entry and the input interface entry should be set to 1. +net.ipv4.conf.all.bc_forwarding=1 +net.ipv4.conf.default.bc_forwarding=0 +  # if a primary address is removed from an interface promote the  # secondary address if available  net.ipv4.conf.all.promote_secondaries=1 diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf new file mode 100644 index 000000000..69eb1a86a --- /dev/null +++ b/src/etc/systemd/system/frr.service.d/override.conf @@ -0,0 +1,11 @@ +[Unit] +Before= +Before=vyos-router.service + +[Service] +ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \ +             echo "log syslog" > /run/frr/config/frr.conf; \ +             echo "log facility local7" >> /run/frr/config/frr.conf; \ +             chown frr:frr /run/frr/config/frr.conf; \ +             chmod 664 /run/frr/config/frr.conf; \ +             mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf' diff --git a/src/etc/systemd/system/logrotate.timer.d/10-override.conf b/src/etc/systemd/system/logrotate.timer.d/10-override.conf new file mode 100644 index 000000000..f50c2b082 --- /dev/null +++ b/src/etc/systemd/system/logrotate.timer.d/10-override.conf @@ -0,0 +1,2 @@ +[Timer] +OnCalendar=hourly diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py new file mode 100755 index 000000000..34accf2cc --- /dev/null +++ b/src/helpers/geoip-update.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import argparse +import sys + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import geoip_update + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = ConfigTreeQuery() +    base = ['firewall'] + +    if not conf.exists(base): +        return None + +    return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, +                                    no_tag_node_value_mangle=True) + +if __name__ == '__main__': +    parser = argparse.ArgumentParser() +    parser.add_argument("--force", help="Force update", action="store_true") +    args = parser.parse_args() + +    firewall = get_config() + +    if not geoip_update(firewall, force=args.force): +        sys.exit(1) diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py new file mode 100755 index 000000000..6b677670b --- /dev/null +++ b/src/helpers/vyos-domain-group-resolve.py @@ -0,0 +1,60 @@ +#!/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 time + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements +from vyos.util import call + + +base = ['firewall', 'group', 'domain-group'] +check_required = True +# count_failed = 0 +# Timeout in sec between checks +timeout = 300 + +domain_state = {} + +if __name__ == '__main__': + +    while check_required: +        config = ConfigTreeQuery() +        if config.exists(base): +            domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) +            for set_name, domain_config in domain_groups.items(): +                list_domains = domain_config['address'] +                elements = [] +                ip_dict = get_ips_domains_dict(list_domains) + +                for domain in list_domains: +                    # Resolution succeeded, update domain state +                    if domain in ip_dict: +                        domain_state[domain] = ip_dict[domain] +                        elements += ip_dict[domain] +                    # Resolution failed, use previous domain state +                    elif domain in domain_state: +                        elements += domain_state[domain] + +                # Resolve successful +                if elements: +                    nft_update_set_elements(f'D_{set_name}', elements) +        time.sleep(timeout) diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 index 5f4cff90d..626d6849f 100755 --- a/src/migration-scripts/firewall/6-to-7 +++ b/src/migration-scripts/firewall/6-to-7 @@ -194,11 +194,12 @@ if config.exists(base + ['ipv6-name']):              if config.exists(rule_icmp + ['type']):                  tmp = config.return_value(rule_icmp + ['type']) -                type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) +                type_code_match = re.match(r'^(\d+)(?:/(\d+))?$', tmp)                  if type_code_match:                      config.set(rule_icmp + ['type'], value=type_code_match[1]) -                    config.set(rule_icmp + ['code'], value=type_code_match[2]) +                    if type_code_match[2]: +                        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: diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 93ce9215f..4095f2a3e 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -20,6 +20,7 @@  import os  import sys  from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN  from vyos.pki import load_certificate  from vyos.pki import load_crl  from vyos.pki import load_dh_parameters @@ -27,6 +28,7 @@ from vyos.pki import load_private_key  from vyos.pki import encode_certificate  from vyos.pki import encode_dh_parameters  from vyos.pki import encode_private_key +from vyos.pki import verify_crl  from vyos.util import run  def wrapped_pem_to_config_value(pem): @@ -129,6 +131,8 @@ if config.exists(base):              config.delete(base + [interface, 'tls', 'crypt-file']) +        ca_certs = {} +          if config.exists(x509_base + ['ca-cert-file']):              if not config.exists(pki_base + ['ca']):                  config.set(pki_base + ['ca']) @@ -136,20 +140,27 @@ if config.exists(base):              cert_file = config.return_value(x509_base + ['ca-cert-file'])              cert_path = os.path.join(AUTH_DIR, cert_file) -            cert = None              if os.path.isfile(cert_path):                  if not os.access(cert_path, os.R_OK):                      run(f'sudo chmod 644 {cert_path}')                  with open(cert_path, 'r') as f: -                    cert_data = f.read() -                    cert = load_certificate(cert_data, wrap_tags=False) - -            if cert: -                cert_pem = encode_certificate(cert) -                config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) -                config.set(x509_base + ['ca-certificate'], value=pki_name) +                    certs_str = f.read() +                    certs_data = certs_str.split(CERT_BEGIN) +                    index = 1 +                    for cert_data in certs_data[1:]: +                        cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + +                        if cert: +                            ca_certs[f'{pki_name}_{index}'] = cert +                            cert_pem = encode_certificate(cert) +                            config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) +                            config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) +                        else: +                            print(f'Failed to migrate CA certificate on openvpn interface {interface}') + +                        index += 1              else:                  print(f'Failed to migrate CA certificate on openvpn interface {interface}') @@ -163,6 +174,7 @@ if config.exists(base):              crl_file = config.return_value(x509_base + ['crl-file'])              crl_path = os.path.join(AUTH_DIR, crl_file)              crl = None +            crl_ca_name = None              if os.path.isfile(crl_path):                  if not os.access(crl_path, os.R_OK): @@ -172,9 +184,14 @@ if config.exists(base):                      crl_data = f.read()                      crl = load_crl(crl_data, wrap_tags=False) -            if crl: +                    for ca_name, ca_cert in ca_certs.items(): +                        if verify_crl(crl, ca_cert): +                            crl_ca_name = ca_name +                            break + +            if crl and crl_ca_name:                  crl_pem = encode_certificate(crl) -                config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) +                config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))              else:                  print(f'Failed to migrate CRL on openvpn interface {interface}') diff --git a/src/migration-scripts/monitoring/0-to-1 b/src/migration-scripts/monitoring/0-to-1 new file mode 100755 index 000000000..803cdb49c --- /dev/null +++ b/src/migration-scripts/monitoring/0-to-1 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process + +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 = ['service', 'monitoring', 'telegraf'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +if config.exists(base + ['authentication', 'organization']): +    tmp = config.return_value(base + ['authentication', 'organization']) +    config.delete(base + ['authentication', 'organization']) +    config.set(base + ['influxdb', 'authentication', 'organization'], value=tmp) + +if config.exists(base + ['authentication', 'token']): +    tmp = config.return_value(base + ['authentication', 'token']) +    config.delete(base + ['authentication', 'token']) +    config.set(base + ['influxdb', 'authentication', 'token'], value=tmp) + +if config.exists(base + ['bucket']): +    tmp = config.return_value(base + ['bucket']) +    config.delete(base + ['bucket']) +    config.set(base + ['influxdb', 'bucket'], value=tmp) + +if config.exists(base + ['port']): +    tmp = config.return_value(base + ['port']) +    config.delete(base + ['port']) +    config.set(base + ['influxdb', 'port'], value=tmp) + +if config.exists(base + ['url']): +    tmp = config.return_value(base + ['url']) +    config.delete(base + ['url']) +    config.set(base + ['influxdb', 'url'], value=tmp) + + +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/system/23-to-24 b/src/migration-scripts/system/23-to-24 index 5ea71d51a..97fe82462 100755 --- a/src/migration-scripts/system/23-to-24 +++ b/src/migration-scripts/system/23-to-24 @@ -20,6 +20,7 @@ from ipaddress import ip_interface  from ipaddress import ip_address  from sys import exit, argv  from vyos.configtree import ConfigTree +from vyos.template import is_ipv4  if (len(argv) < 1):      print("Must specify file name!") @@ -37,6 +38,9 @@ def fixup_cli(config, path, interface):      if config.exists(path + ['address']):          for address in config.return_values(path + ['address']):              tmp = ip_interface(address) +            # ARP is only available for IPv4 ;-) +            if not is_ipv4(tmp): +                continue              if ip_address(host) in tmp.network.hosts():                  mac = config.return_value(tmp_base + [host, 'hwaddr'])                  iface_path = ['protocols', 'static', 'arp', 'interface'] diff --git a/src/migration-scripts/system/24-to-25 b/src/migration-scripts/system/24-to-25 new file mode 100755 index 000000000..c2f70689d --- /dev/null +++ b/src/migration-scripts/system/24-to-25 @@ -0,0 +1,52 @@ +#!/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/>. +# +# Migrate system syslog global archive to system logs logrotate messages + +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', 'syslog', 'global', 'archive'] +config = ConfigTree(config_file) + +if not config.exists(base): +    exit(0) + +if config.exists(base + ['file']): +    tmp = config.return_value(base + ['file']) +    config.set(['system', 'logs', 'logrotate', 'messages', 'rotate'], value=tmp) + +if config.exists(base + ['size']): +    tmp = config.return_value(base + ['size']) +    tmp = max(round(int(tmp) / 1024), 1) # kb -> mb +    config.set(['system', 'logs', 'logrotate', 'messages', 'max-size'], value=tmp) + +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/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py new file mode 100755 index 000000000..250dbcce1 --- /dev/null +++ b/src/op_mode/clear_dhcp_lease.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import argparse +import re + +from isc_dhcp_leases import Lease +from isc_dhcp_leases import IscDhcpLeases + +from vyos.configquery import ConfigTreeQuery +from vyos.util import ask_yes_no +from vyos.util import call +from vyos.util import commit_in_progress + + +config = ConfigTreeQuery() +base = ['service', 'dhcp-server'] +lease_file = '/config/dhcpd.leases' + + +def del_lease_ip(address): +    """ +    Read lease_file and write data to this file +    without specific section "lease ip" +    Delete section "lease x.x.x.x { x;x;x; }" +    """ +    with open(lease_file, encoding='utf-8') as f: +        data = f.read().rstrip() +        lease_config_ip = '{(?P<config>[\s\S]+?)\n}' +        pattern = rf"lease {address} {lease_config_ip}" +        # Delete lease for ip block +        data = re.sub(pattern, '', data) + +    # Write new data to original lease_file +    with open(lease_file, 'w', encoding='utf-8') as f: +        f.write(data) + +def is_ip_in_leases(address): +    """ +    Return True if address found in the lease file +    """ +    leases = IscDhcpLeases(lease_file) +    lease_ips = [] +    for lease in leases.get(): +        lease_ips.append(lease.ip) +    if address not in lease_ips: +        print(f'Address "{address}" not found in "{lease_file}"') +        return False +    return True + + +if not config.exists(base): +    print('DHCP-server not configured!') +    exit(0) + +if config.exists(base + ['failover']): +    print('Lease cannot be reset in failover mode!') +    exit(0) + + +if __name__ == '__main__': +    parser = argparse.ArgumentParser() +    parser.add_argument('--ip', help='IPv4 address', action='store', required=True) + +    args = parser.parse_args() +    address = args.ip + +    if not is_ip_in_leases(address): +        exit(1) + +    if commit_in_progress(): +        print('Cannot clear DHCP lease while a commit is in progress') +        exit(1) + +    if not ask_yes_no(f'This will restart DHCP server.\nContinue?'): +        exit(1) +    else: +        del_lease_ip(address) +        call('systemctl restart isc-dhcp-server.service') diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index ffc574362..936c20bcb 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -20,6 +20,7 @@ import argparse  from psutil import process_iter  from vyos.util import call +from vyos.util import commit_in_progress  from vyos.util import DEVNULL  from vyos.util import is_wwan_connected @@ -87,6 +88,9 @@ def main():      args = parser.parse_args()      if args.connect: +        if commit_in_progress(): +            print('Cannot connect while a commit is in progress') +            exit(1)          connect(args.connect)      elif args.disconnect:          disconnect(args.disconnect) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index e45c38f07..54ecd6d0e 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -22,6 +22,7 @@ from argparse import ArgumentParser  from vyos.configquery import CliShellApiConfigQuery  from vyos.configquery import ConfigTreeQuery  from vyos.util import call +from vyos.util import commit_in_progress  from vyos.util import cmd  from vyos.util import run  from vyos.template import render_to_string @@ -86,6 +87,9 @@ if __name__ == '__main__':      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') diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 3146fc357..0aea17b3a 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -270,7 +270,7 @@ def show_firewall_group(name=None):              references = find_references(group_type, group_name)              row = [group_name, group_type, '\n'.join(references) or 'N/A']              if 'address' in group_conf: -                row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address))) +                row.append("\n".join(sorted(group_conf['address'])))              elif 'network' in group_conf:                  row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))              elif 'mac_address' in group_conf: diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 6586cbceb..514143cd7 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -22,7 +22,9 @@ import ipaddress  import os.path  from tabulate import tabulate  from json import loads -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import commit_in_progress +from vyos.util import run  from vyos.logger import syslog  # some default values @@ -224,6 +226,9 @@ if not _uacctd_running():  # restart pmacct daemon  if cmd_args.action == 'restart': +    if commit_in_progress(): +        print('Cannot restart flow-accounting while a commit is in progress') +        exit(1)      # run command to restart flow-accounting      cmd('systemctl restart uacctd.service',          message='Failed to restart flow-accounting') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index cbc9ef973..43e94048d 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -17,10 +17,15 @@  from sys import exit  from vyos.util import ask_yes_no  from vyos.util import cmd +from vyos.util import commit_in_progress  if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):      exit(0) +if commit_in_progress(): +    print('Cannot restart SSH while a commit is in progress') +    exit(1) +  cmd('rm -v /etc/ssh/ssh_host_*')  cmd('dpkg-reconfigure openssh-server')  cmd('systemctl restart ssh.service') diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py index c3cd25186..a128cc011 100755 --- a/src/op_mode/openconnect-control.py +++ b/src/op_mode/openconnect-control.py @@ -19,7 +19,10 @@ import argparse  import json  from vyos.config import Config -from vyos.util import popen, run, DEVNULL +from vyos.util import commit_in_progress +from vyos.util import popen +from vyos.util import run +from vyos.util import DEVNULL  from tabulate import tabulate  occtl        = '/usr/bin/occtl' @@ -57,6 +60,10 @@ def main():      # Check is Openconnect server configured      is_ocserv_configured() +    if commit_in_progress(): +        print('Cannot restart openconnect while a commit is in progress') +        exit(1) +      if args.action == "restart":          run("sudo systemctl restart ocserv.service")          sys.exit(0) diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index dbd3eb4d1..efbf65083 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -17,6 +17,7 @@  import os  from sys import argv, exit  from vyos.util import call +from vyos.util import commit_in_progress  if __name__ == '__main__':      if (len(argv) < 1): @@ -25,6 +26,9 @@ if __name__ == '__main__':      interface = argv[1]      if os.path.isfile(f'/run/openvpn/{interface}.conf'): +        if commit_in_progress(): +            print('Cannot restart OpenVPN while a commit is in progress') +            exit(1)          call(f'systemctl restart openvpn@{interface}.service')      else:          print(f'OpenVPN interface "{interface}" does not exist!') diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index af4fb2d15..db5a48970 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -24,6 +24,7 @@ import os  import vyos.config  from vyos.util import call +from vyos.util import commit_in_progress  parser = argparse.ArgumentParser() @@ -39,6 +40,9 @@ if __name__ == '__main__':          if not c.exists_effective('service dhcp-relay'):              print("DHCP relay service not configured")          else: +            if commit_in_progress(): +                print('Cannot restart DHCP relay while a commit is in progress') +                exit(1)              call('systemctl restart isc-dhcp-server.service')          sys.exit(0) @@ -47,6 +51,9 @@ if __name__ == '__main__':          if not c.exists_effective('service dhcpv6-relay'):              print("DHCPv6 relay service not configured")          else: +            if commit_in_progress(): +                print('Cannot restart DHCPv6 relay while commit is in progress') +                exit(1)              call('systemctl restart isc-dhcp-server6.service')          sys.exit(0) diff --git a/src/op_mode/show_conntrack.py b/src/op_mode/show_conntrack.py new file mode 100755 index 000000000..089a3e454 --- /dev/null +++ b/src/op_mode/show_conntrack.py @@ -0,0 +1,102 @@ +#!/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 xmltodict + +from tabulate import tabulate +from vyos.util import cmd + + +def _get_raw_data(): +    """ +    Get conntrack XML output +    """ +    return cmd(f'sudo  conntrack --dump --output xml') + + +def _xml_to_dict(xml): +    """ +    Convert XML to dictionary +    Return: dictionary +    """ +    parse = xmltodict.parse(xml) +    # If only one conntrack entry we must change dict +    if 'meta' in parse['conntrack']['flow']: +        return dict(conntrack={'flow': [parse['conntrack']['flow']]}) +    return parse + + +def _get_formatted_output(xml): +    """ +    :param xml: +    :return: formatted output +    """ +    data_entries = [] +    dict_data = _xml_to_dict(xml) +    for entry in dict_data['conntrack']['flow']: +        orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {} +        reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {} +        proto = {} +        for meta in entry['meta']: +            direction = meta['@direction'] +            if direction in ['original']: +                if 'layer3' in meta: +                    orig_src = meta['layer3']['src'] +                    orig_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        orig_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        orig_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['@protoname'] +            if direction in ['reply']: +                if 'layer3' in meta: +                    reply_src = meta['layer3']['src'] +                    reply_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        reply_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        reply_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['@protoname'] +            if direction == 'independent': +                conn_id = meta['id'] +                timeout = meta['timeout'] +                orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src +                orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst +                reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src +                reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst +                state = meta['state'] if 'state' in meta else '' +                mark = meta['mark'] +                zone = meta['zone'] if 'zone' in meta else '' +                data_entries.append( +                    [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone]) +    headers = ["Id", "Original src", "Original dst", "Reply src", "Reply dst", "Protocol", "State", "Timeout", "Mark", +               "Zone"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def show(raw: bool): +    conntrack_data = _get_raw_data() +    if raw: +        return conntrack_data +    else: +        return _get_formatted_output(conntrack_data) + + +if __name__ == '__main__': +    print(show(raw=False)) diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py index 98adb31dd..60a4bdd13 100755 --- a/src/op_mode/show_nat_rules.py +++ b/src/op_mode/show_nat_rules.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 @@ -99,8 +99,9 @@ if args.source or args.destination:                      if addr_tmp and len_tmp:                          tran_addr += addr_tmp + '/' + str(len_tmp) + ' ' -                if isinstance(tran_addr_json['port'],int): -                    tran_addr += 'port ' + str(tran_addr_json['port']) +                if tran_addr_json.get('port'): +                    if isinstance(tran_addr_json['port'],int): +                        tran_addr += 'port ' + str(tran_addr_json['port'])              else:                  if 'masquerade' in data['expr'][i]: @@ -111,6 +112,8 @@ if args.source or args.destination:          if srcdest != '':              srcdests.append(srcdest)              srcdest = '' +        else: +            srcdests.append('any')          print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface))          for i in range(1, len(srcdests)): diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py index 25091e9fc..508845e23 100755 --- a/src/op_mode/show_nat_translations.py +++ b/src/op_mode/show_nat_translations.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 @@ -83,11 +83,23 @@ def pipe():      return xml +def xml_to_dict(xml): +    """ +    Convert XML to dictionary +    Return: dictionary +    """ +    parse = xmltodict.parse(xml) +    # If only one NAT entry we must change dict T4499 +    if 'meta' in parse['conntrack']['flow']: +        return dict(conntrack={'flow': [parse['conntrack']['flow']]}) +    return parse + +  def process(data, stats, protocol, pipe, verbose, flowtype=''):      if not data:          return -    parsed = xmltodict.parse(data) +    parsed = xml_to_dict(data)      print(headers(verbose, pipe)) diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh index 09980e14f..25d09ce77 100755 --- a/src/op_mode/vtysh_wrapper.sh +++ b/src/op_mode/vtysh_wrapper.sh @@ -1,5 +1,6 @@  #!/bin/sh  declare -a tmp -# FRR uses ospf6 where we use ospfv3, thus alter the command -tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/") +# FRR uses ospf6 where we use ospfv3, and we use reset over clear for BGP, +# thus alter the commands +tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/")  vtysh -c "$tmp" diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py new file mode 100755 index 000000000..1c85380bc --- /dev/null +++ b/src/system/vyos-event-handler.py @@ -0,0 +1,162 @@ +#!/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 json +import re +import select +from copy import deepcopy +from os import getpid, environ +from pathlib import Path +from signal import signal, SIGTERM, SIGINT +from sys import exit +from systemd import journal + +from vyos.util import run, dict_search + +# Identify this script +my_pid = getpid() +my_name = Path(__file__).stem + + +# handle termination signal +def handle_signal(signal_type, frame): +    if signal_type == SIGTERM: +        journal.send('Received SIGTERM signal, stopping normally', +                     SYSLOG_IDENTIFIER=my_name) +    if signal_type == SIGINT: +        journal.send('Received SIGINT signal, stopping normally', +                     SYSLOG_IDENTIFIER=my_name) +    exit(0) + + +# Class for analyzing and process messages +class Analyzer: +    # Initialize settings +    def __init__(self, config: dict) -> None: +        self.config = {} +        # Prepare compiled regex objects +        for event_id, event_config in config.items(): +            script = dict_search('script.path', event_config) +            # Check for arguments +            if dict_search('script.arguments', event_config): +                script_arguments = dict_search('script.arguments', event_config) +                script = f'{script} {script_arguments}' +            # Prepare environment +            environment = deepcopy(environ) +            # Check for additional environment options +            if dict_search('script.environment', event_config): +                for env_variable, env_value in dict_search( +                        'script.environment', event_config).items(): +                    environment[env_variable] = env_value.get('value') +            # Create final config dictionary +            pattern_raw = event_config['filter']['pattern'] +            pattern_compiled = re.compile( +                rf'{event_config["filter"]["pattern"]}') +            pattern_config = { +                pattern_compiled: { +                    'pattern_raw': +                        pattern_raw, +                    'syslog_id': +                        dict_search('filter.syslog-identifier', event_config), +                    'pattern_script': { +                        'path': script, +                        'environment': environment +                    } +                } +            } +            self.config.update(pattern_config) + +    # Execute script safely +    def script_run(self, pattern: str, script_path: str, +                   script_env: dict) -> None: +        try: +            run(script_path, env=script_env) +            journal.send( +                f'Pattern found: "{pattern}", script executed: "{script_path}"', +                SYSLOG_IDENTIFIER=my_name) +        except Exception as err: +            journal.send( +                f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}', +                SYSLOG_IDENTIFIER=my_name) + +    # Analyze a message +    def process_message(self, message: dict) -> None: +        for pattern_compiled, pattern_config in self.config.items(): +            # Check if syslog id is presented in config and matches +            syslog_id = pattern_config.get('syslog_id') +            if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id: +                continue +            if pattern_compiled.fullmatch(message['MESSAGE']): +                # Add message to environment variables +                pattern_config['pattern_script']['environment'][ +                    'message'] = message['MESSAGE'] +                # Run script +                self.script_run( +                    pattern=pattern_config['pattern_raw'], +                    script_path=pattern_config['pattern_script']['path'], +                    script_env=pattern_config['pattern_script']['environment']) + + +if __name__ == '__main__': +    # Parse command arguments and get config +    parser = argparse.ArgumentParser() +    parser.add_argument('-c', +                        '--config', +                        action='store', +                        help='Path to even-handler configuration', +                        required=True, +                        type=Path) + +    args = parser.parse_args() +    try: +        config_path = Path(args.config) +        config = json.loads(config_path.read_text()) +        # Create an object for analazyng messages +        analyzer = Analyzer(config) +    except Exception as err: +        print( +            f'Configuration file "{config_path}" does not exist or malformed: {err}' +        ) +        exit(1) + +    # Prepare for proper exitting +    signal(SIGTERM, handle_signal) +    signal(SIGINT, handle_signal) + +    # Set up journal connection +    data = journal.Reader() +    data.seek_tail() +    data.get_previous() +    p = select.poll() +    p.register(data, data.get_events()) + +    journal.send(f'Started with configuration: {config}', +                 SYSLOG_IDENTIFIER=my_name) + +    while p.poll(): +        if data.process() != journal.APPEND: +            continue +        for entry in data: +            message = entry['MESSAGE'] +            pid = entry['_PID'] +            # Skip empty messages and messages from this process +            if message and pid != my_pid: +                try: +                    analyzer.process_message(entry) +                except Exception as err: +                    journal.send(f'Unable to process message: {err}', +                                 SYSLOG_IDENTIFIER=my_name) diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 2ced1038a..23cd4cfc3 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -13,6 +13,9 @@ PIDFile=/var/lib/dhcp/dhclient_%i.pid  ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS  ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r  Restart=always +TimeoutStopSec=20 +SendSIGKILL=true +FinalKillSignal=SIGABRT  [Install]  WantedBy=multi-user.target diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service new file mode 100644 index 000000000..29628fddb --- /dev/null +++ b/src/systemd/vyos-domain-group-resolve.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS firewall domain-group resolver +After=vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-event-handler.service b/src/systemd/vyos-event-handler.service new file mode 100644 index 000000000..6afe4f95b --- /dev/null +++ b/src/systemd/vyos-event-handler.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS event handler +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-event-handler.py --config /run/vyos-event-handler.conf + +[Install] +WantedBy=multi-user.target | 
