diff options
22 files changed, 660 insertions, 84 deletions
| diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2 index 005338836..b9f77a257 100644 --- a/data/templates/ids/fastnetmon.j2 +++ b/data/templates/ids/fastnetmon.j2 @@ -5,6 +5,9 @@ logging:local_syslog_logging = on  # list of all your networks in CIDR format  networks_list_path = /run/fastnetmon/networks_list +# list networks in CIDR format which will be not monitored for attacks +white_list_path = /run/fastnetmon/excluded_networks_list +  # Enable/Disable any actions in case of attack  enable_ban = on  enable_ban_ipv6 = on diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2 new file mode 100644 index 000000000..c88a1c527 --- /dev/null +++ b/data/templates/ids/fastnetmon_excluded_networks_list.j2 @@ -0,0 +1,5 @@ +{% if excluded_network is vyos_defined %} +{%     for net in excluded_network %} +{{ net }} +{%     endfor %} +{% endif %} diff --git a/interface-definitions/service-ids-ddos-protection.xml.in b/interface-definitions/service-ids-ddos-protection.xml.in index a176d6fff..86fc4dffa 100644 --- a/interface-definitions/service-ids-ddos-protection.xml.in +++ b/interface-definitions/service-ids-ddos-protection.xml.in @@ -43,6 +43,24 @@                    <multi/>                  </properties>                </leafNode> +              <leafNode name="excluded-network"> +                <properties> +                  <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help> +                  <valueHelp> +                    <format>ipv4net</format> +                    <description>IPv4 prefix(es) to exclude</description> +                  </valueHelp> +                  <valueHelp> +                    <format>ipv6net</format> +                    <description>IPv6 prefix(es) to exclude</description> +                  </valueHelp> +                  <constraint> +                    <validator name="ipv4-prefix"/> +                    <validator name="ipv6-prefix"/> +                  </constraint> +                  <multi/> +                </properties> +              </leafNode>                <leafNode name="listen-interface">                  <properties>                    <help>Listen interface for mirroring traffic</help> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index 084e2e7e3..b0ec8989f 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -16,13 +16,13 @@                  <properties>                    <help>Show configured source NAT rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/show_nat_rules.py --source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source</command>                </node>                <node name="statistics">                  <properties>                    <help>Show statistics for configured source NAT rules</help>                  </properties> -                <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command>                </node>                <node name="translations">                  <properties> diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in index 0f8d3064d..dd2a28931 100644 --- a/op-mode-definitions/show-bridge.xml.in +++ b/op-mode-definitions/show-bridge.xml.in @@ -11,7 +11,7 @@              <properties>                <help>View the VLAN filter settings of the bridge</help>              </properties> -            <command>bridge -c vlan show</command> +            <command>${vyos_op_scripts_dir}/bridge.py show_vlan</command>            </leafNode>          </children>        </node> @@ -19,7 +19,7 @@          <properties>            <help>Show bridging information</help>          </properties> -        <command>bridge -c link show</command> +        <command>${vyos_op_scripts_dir}/bridge.py show</command>        </leafNode>        <tagNode name="bridge">          <properties> @@ -34,13 +34,13 @@              <properties>                <help>Displays the multicast group database for the bridge</help>              </properties> -            <command>bridge -c mdb show dev $3</command> +            <command>${vyos_op_scripts_dir}/bridge.py show_mdb --interface=$3</command>            </leafNode>            <leafNode name="fdb">              <properties>                <help>Show the forwarding database of the bridge</help>              </properties> -            <command>bridge -c fdb show br $3</command> +            <command>${vyos_op_scripts_dir}/bridge.py show_fdb --interface=$3</command>            </leafNode>          </children>        </tagNode> diff --git a/op-mode-definitions/show-vrf.xml.in b/op-mode-definitions/show-vrf.xml.in index 9c38c30fe..0e0370445 100644 --- a/op-mode-definitions/show-vrf.xml.in +++ b/op-mode-definitions/show-vrf.xml.in @@ -6,7 +6,7 @@          <properties>            <help>Show VRF information</help>          </properties> -        <command>${vyos_op_scripts_dir}/show_vrf.py -e</command> +        <command>${vyos_op_scripts_dir}/vrf.py show</command>        </node>        <tagNode name="vrf">          <properties> @@ -15,7 +15,7 @@              <path>vrf name</path>            </completionHelp>          </properties> -        <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> +        <command>${vyos_op_scripts_dir}/vrf.py show --name="$3"</command>          <children>            <leafNode name="processes">              <properties> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index 3d997c143..a98cf8ff2 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -19,16 +19,16 @@                  <properties>                    <help>Reset a specific tunnel for given peer</help>                  </properties> -                <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="$6"</command> +                <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="$6"</command>                </tagNode>                <node name="vti">                  <properties>                    <help>Reset the VTI tunnel for given peer</help>                  </properties> -                <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="vti"</command> +                <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="vti"</command>                </node>              </children> -            <command>sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="reset-peer" --name="$4" --tunnel="all"</command> +            <command>sudo ${vyos_op_scripts_dir}/ipsec.py reset_peer --peer="$4" --tunnel="all"</command>            </tagNode>            <tagNode name="ipsec-profile">              <properties> @@ -76,6 +76,9 @@                <tagNode name="peer">                  <properties>                    <help>Show debugging information for a peer</help> +                  <completionHelp> +                    <path>vpn ipsec site-to-site peer</path> +                  </completionHelp>                  </properties>                  <children>                    <tagNode name="tunnel"> diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 9c0c93779..009dbc803 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -921,5 +921,31 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):          self.assertIn(f' neighbor {peer_group} peer-group', frrconfig)          self.assertIn(f' neighbor {peer_group} remote-as {remote_asn}', frrconfig) +    def test_bgp_15_local_as_ebgp(self): +        # https://phabricator.vyos.net/T4560 +        # local-as allowed only for ebgp peers + +        neighbor = '192.0.2.99' +        remote_asn = '500' +        local_asn = '400' + +        self.cli_set(base_path + ['local-as', ASN]) +        self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN]) +        self.cli_set(base_path + ['neighbor', neighbor, 'local-as', local_asn]) + +        # check validate() - local-as allowed only for ebgp peers +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn]) + +        self.cli_commit() + +        frrconfig = self.getFRRconfig(f'router bgp {ASN}') +        self.assertIn(f'router bgp {ASN}', frrconfig) +        self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig) +        self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig) + +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ids.py b/smoketest/scripts/cli/test_service_ids.py index 8720362ba..d471eeaed 100755 --- a/smoketest/scripts/cli/test_service_ids.py +++ b/smoketest/scripts/cli/test_service_ids.py @@ -26,6 +26,7 @@ from vyos.util import read_file  PROCESS_NAME = 'fastnetmon'  FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf'  NETWORKS_CONF = '/run/fastnetmon/networks_list' +EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list'  base_path = ['service', 'ids', 'ddos-protection']  class TestServiceIDS(VyOSUnitTestSHIM.TestCase): @@ -50,6 +51,7 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):      def test_fastnetmon(self):          networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64'] +        excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128']          interfaces = ['eth0', 'eth1']          fps = '3500'          mbps = '300' @@ -62,6 +64,12 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):          for tmp in networks:              self.cli_set(base_path + ['network', tmp]) +        # optional excluded-network! +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        for tmp in excluded_networks: +            self.cli_set(base_path + ['excluded-network', tmp]) +          # Required interface(s)!          with self.assertRaises(ConfigSessionError):              self.cli_commit() @@ -100,5 +108,9 @@ class TestServiceIDS(VyOSUnitTestSHIM.TestCase):          for tmp in networks:              self.assertIn(f'{tmp}', network_config) +        excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF) +        for tmp in excluded_networks: +            self.assertIn(f'{tmp}', excluded_network_config) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 7750c1247..7e16235c1 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -192,6 +192,11 @@ def verify(flow_config):                      raise ConfigError("All sFlow servers must use the same IP protocol")              else:                  sflow_collector_ipver = ip_address(server).version +	 +        # check if vrf is defined for Sflow +        sflow_vrf = None +        if 'vrf' in flow_config: +            sflow_vrf = flow_config['vrf']          # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa          for server in flow_config['sflow']['server']: @@ -203,12 +208,12 @@ def verify(flow_config):          if 'agent_address' in flow_config['sflow']:              tmp = flow_config['sflow']['agent_address'] -            if not is_addr_assigned(tmp): +            if not is_addr_assigned(tmp, sflow_vrf):                  raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')          # Check if configured netflow source-address exist in the system          if 'source_address' in flow_config['sflow']: -            if not is_addr_assigned(flow_config['sflow']['source_address']): +            if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):                  tmp = flow_config['sflow']['source_address']                  raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!') diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index bf0f6840d..af2d0588d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -118,6 +118,11 @@ def verify(vxlan):              # in use.              vxlan_overhead += 20 +        # If source_address is not used - check IPv6 'remote' list +        elif 'remote' in vxlan: +            if any(is_ipv6(a) for a in vxlan['remote']): +                vxlan_overhead += 20 +          lower_mtu = Interface(vxlan['source_interface']).get_mtu()          if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):              raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\ diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index e275ace84..97b3a6396 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -76,7 +76,7 @@ def get_config(config=None):      # We need to know the amount of other WWAN interfaces as ModemManager needs      # to be started or stopped.      conf.set_level(base) -    _, wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), +    wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),                                                         get_first_key=True,                                                         no_tag_node_value_mangle=True) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 5aa643476..7d3687094 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -213,6 +213,12 @@ def verify(bgp):                      if 'source_interface' in peer_config['interface']:                          raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') +            # Local-AS allowed only for EBGP peers +            if 'local_as' in peer_config: +                remote_as = verify_remote_as(peer_config, bgp) +                if remote_as == bgp['local_as']: +                    raise ConfigError(f'local-as configured for "{peer}", allowed only for eBGP peers!') +              for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',                          'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',                          'l2vpn_evpn']: diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index 615658c84..c58f8db9a 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -29,6 +29,7 @@ airbag.enable()  config_file = r'/run/fastnetmon/fastnetmon.conf'  networks_list = r'/run/fastnetmon/networks_list' +excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'  def get_config(config=None):      if config: @@ -75,6 +76,7 @@ def generate(fastnetmon):      render(config_file, 'ids/fastnetmon.j2', fastnetmon)      render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) +    render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)      return None  def apply(fastnetmon): diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index 5a64dade8..8274e6564 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -62,7 +62,7 @@ def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None:      for route_item in route_info_data:          route_dev = route_item.get('dev')          route_dst = route_item.get('dst') -        route_gateway = route_item.get('route_gateway') +        route_gateway = route_item.get('gateway')          # Prepare a command to add a route          route_add_cmd = 'sudo ip route add'          if route_dst: diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf index 8f7f3774f..841666070 100644 --- a/src/etc/systemd/system/fastnetmon.service.d/override.conf +++ b/src/etc/systemd/system/fastnetmon.service.d/override.conf @@ -7,6 +7,6 @@ After=vyos-router.service  [Service]  Type=simple  WorkingDirectory=/run/fastnetmon -PIDFile=/run/fastnetmon/fastnetmon.pid +PIDFile=/run/fastnetmon.pid  ExecStart=  ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py new file mode 100755 index 000000000..411aa06d1 --- /dev/null +++ b/src/op_mode/bridge.py @@ -0,0 +1,202 @@ +#!/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 jmespath +import json +import sys +import typing + +from sys import exit +from tabulate import tabulate + +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode + + +def _get_json_data(): +    """ +    Get bridge data format JSON +    """ +    return cmd(f'sudo bridge --json link show') + + +def _get_raw_data_summary(): +    """Get interested rules +    :returns dict +    """ +    data = _get_json_data() +    data_dict = json.loads(data) +    return data_dict + + +def _get_raw_data_vlan(): +    """ +    :returns dict +    """ +    json_data = cmd('sudo bridge --json --compressvlans vlan show') +    data_dict = json.loads(json_data) +    return data_dict + + +def _get_raw_data_fdb(bridge): +    """Get MAC-address for the bridge brX +    :returns list +    """ +    json_data = cmd(f'sudo bridge --json fdb show br {bridge}') +    data_dict = json.loads(json_data) +    return data_dict + + +def _get_raw_data_mdb(bridge): +    """Get MAC-address multicast gorup for the bridge brX +    :return list +    """ +    json_data = cmd(f'bridge --json  mdb show br {bridge}') +    data_dict = json.loads(json_data) +    return data_dict + + +def _get_bridge_members(bridge: str) -> list: +    """ +    Get list of interface bridge members +    :param bridge: str +    :default: ['n/a'] +    :return: list +    """ +    data = _get_raw_data_summary() +    members = jmespath.search(f'[?master == `{bridge}`].ifname', data) +    return [member for member in members] if members else ['n/a'] + + +def _get_member_options(bridge: str): +    data = _get_raw_data_summary() +    options = jmespath.search(f'[?master == `{bridge}`]', data) +    return options + + +def _get_formatted_output_summary(data): +    data_entries = '' +    bridges = set(jmespath.search('[*].master', data)) +    for bridge in bridges: +        member_options = _get_member_options(bridge) +        member_entries = [] +        for option in member_options: +            interface = option.get('ifname') +            ifindex = option.get('ifindex') +            state = option.get('state') +            mtu = option.get('mtu') +            flags = ','.join(option.get('flags')).lower() +            prio = option.get('priority') +            member_entries.append([interface, state, mtu, flags, prio]) +        member_headers = ["Member", "State", "MTU", "Flags", "Prio"] +        output_members = tabulate(member_entries, member_headers, numalign="left") +        output_bridge = f"""Bridge interface {bridge}: +{output_members} + +""" +        data_entries += output_bridge +    output = data_entries +    return output + + +def _get_formatted_output_vlan(data): +    data_entries = [] +    for entry in data: +        interface = entry.get('ifname') +        vlans = entry.get('vlans') +        for vlan_entry in vlans: +            vlan = vlan_entry.get('vlan') +            if vlan_entry.get('vlanEnd'): +                vlan_end = vlan_entry.get('vlanEnd') +                vlan = f'{vlan}-{vlan_end}' +            flags = ', '.join(vlan_entry.get('flags')).lower() +            data_entries.append([interface, vlan, flags]) + +    headers = ["Interface", "Vlan", "Flags"] +    output = tabulate(data_entries, headers) +    return output + + +def _get_formatted_output_fdb(data): +    data_entries = [] +    for entry in data: +        interface = entry.get('ifname') +        mac = entry.get('mac') +        state = entry.get('state') +        flags = ','.join(entry['flags']) +        data_entries.append([interface, mac, state, flags]) + +    headers = ["Interface", "Mac address", "State", "Flags"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def _get_formatted_output_mdb(data): +    data_entries = [] +    for entry in data: +        for mdb_entry in entry['mdb']: +            interface = mdb_entry.get('port') +            group = mdb_entry.get('grp') +            state = mdb_entry.get('state') +            flags = ','.join(mdb_entry.get('flags')) +            data_entries.append([interface, group, state, flags]) +    headers = ["Interface", "Group", "State", "Flags"] +    output = tabulate(data_entries, headers) +    return output + + +def show(raw: bool): +    bridge_data = _get_raw_data_summary() +    if raw: +        return bridge_data +    else: +        return _get_formatted_output_summary(bridge_data) + + +def show_vlan(raw: bool): +    bridge_vlan = _get_raw_data_vlan() +    if raw: +        return bridge_vlan +    else: +        return _get_formatted_output_vlan(bridge_vlan) + + +def show_fdb(raw: bool, interface: str): +    fdb_data = _get_raw_data_fdb(interface) +    if raw: +        return fdb_data +    else: +        return _get_formatted_output_fdb(fdb_data) + + +def show_mdb(raw: bool, interface: str): +    mdb_data = _get_raw_data_mdb(interface) +    if raw: +        return mdb_data +    else: +        return _get_formatted_output_mdb(mdb_data) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except ValueError as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py new file mode 100755 index 000000000..432856585 --- /dev/null +++ b/src/op_mode/ipsec.py @@ -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/>. + +import re +import sys +from vyos.util import call +import vyos.opmode + + +SWANCTL_CONF = '/etc/swanctl/swanctl.conf' + + +def get_peer_connections(peer, tunnel, return_all = False): +    peer = peer.replace(':', '-') +    search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' +    matches = [] +    with open(SWANCTL_CONF, 'r') as f: +        for line in f.readlines(): +            result = re.match(search, line) +            if result: +                suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel +                if return_all or (result[2] == suffix): +                    matches.append(result[1]) +    return matches + + +def reset_peer(peer: str, tunnel:str): +    if not peer: +        print('Invalid peer, aborting') +        return + +    conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) + +    if not conns: +        print('Tunnel(s) not found, aborting') +        return + +    result = True +    for conn in conns: +        try: +            call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) +            call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) +        except TimeoutExpired as e: +            print(f'Timed out while resetting {conn}') +            result = False + + +    print('Peer reset result: ' + ('success' if result else 'failed')) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except ValueError as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py new file mode 100755 index 000000000..4b54ecf31 --- /dev/null +++ b/src/op_mode/nat.py @@ -0,0 +1,191 @@ +#!/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 jmespath +import json +import sys + +from sys import exit +from tabulate import tabulate + +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode + + +def _get_json_data(direction): +    """ +    Get NAT format JSON +    """ +    if direction == 'source': +        chain = 'POSTROUTING' +    if direction == 'destination': +        chain = 'PREROUTING' +    return cmd(f'sudo nft --json list chain ip nat {chain}') + + +def _get_raw_data_rules(direction): +    """Get interested rules +    :returns dict +    """ +    data = _get_json_data(direction) +    data_dict = json.loads(data) +    rules = [] +    for rule in data_dict['nftables']: +        if 'rule' in rule and 'comment' in rule['rule']: +            rules.append(rule) +    return rules + + +def _get_formatted_output_rules(data, direction): +    # Add default values before loop +    sport, dport, proto = 'any', 'any', 'any' +    saddr, daddr = '0.0.0.0/0', '0.0.0.0/0' +    data_entries = [] +    for rule in data: +        if 'comment' in rule['rule']: +            comment = rule.get('rule').get('comment') +            rule_number = comment.split('-')[-1] +            rule_number = rule_number.split(' ')[0] +        if 'expr' in rule['rule']: +            interface = rule.get('rule').get('expr')[0].get('match').get('right') \ +                if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any' +        for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)): +            if 'payload' in match['left']: +                if 'prefix' in match['right'] or 'set' in match['right']: +                    # Merge dict src/dst l3_l4 parameters +                    my_dict = {**match['left']['payload'], **match['right']} +                    proto = my_dict.get('protocol').upper() +                    if my_dict['field'] == 'saddr': +                        saddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' +                    elif my_dict['field'] == 'daddr': +                        daddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' +                    elif my_dict['field'] == 'sport': +                        # Port range or single port +                        if jmespath.search('set[*].range', my_dict): +                            sport = my_dict['set'][0]['range'] +                            sport = '-'.join(map(str, sport)) +                        else: +                            sport = my_dict.get('set') +                            sport = ','.join(map(str, sport)) +                    elif my_dict['field'] == 'dport': +                        # Port range or single port +                        if jmespath.search('set[*].range', my_dict): +                            dport = my_dict["set"][0]["range"] +                            dport = '-'.join(map(str, dport)) +                        else: +                            dport = my_dict.get('set') +                            dport = ','.join(map(str, dport)) +                else: +                    if jmespath.search('left.payload.field', match) == 'saddr': +                        saddr = match.get('right') +                    if jmespath.search('left.payload.field', match) == 'daddr': +                        daddr = match.get('right') +            else: +                saddr = '0.0.0.0/0' +                daddr = '0.0.0.0/0' +                sport = 'any' +                dport = 'any' +                proto = 'any' + +            source = f'''{saddr} +sport {sport}''' +            destination = f'''{daddr} +dport {dport}''' + +            if jmespath.search('left.payload.field', match) == 'protocol': +                field_proto = match.get('right').upper() + +            for expr in rule.get('rule').get('expr'): +                if 'snat' in expr: +                    translation = dict_search('snat.addr', expr) +                    if expr['snat'] and 'port' in expr['snat']: +                        if jmespath.search('snat.port.range', expr): +                            port = dict_search('snat.port.range', expr) +                            port = '-'.join(map(str, port)) +                        else: +                            port = expr['snat']['port'] +                        translation = f'''{translation} +port {port}''' + +                elif 'masquerade' in expr: +                    translation = 'masquerade' +                    if expr['masquerade'] and 'port' in expr['masquerade']: +                        if jmespath.search('masquerade.port.range', expr): +                            port = dict_search('masquerade.port.range', expr) +                            port = '-'.join(map(str, port)) +                        else: +                            port = expr['masquerade']['port'] + +                        translation = f'''{translation} +port {port}''' +                else: +                    translation = 'exclude' +        # Overwrite match loop 'proto' if specified filed 'protocol' exist +        if 'protocol' in jmespath.search('rule.expr[*].match.left.payload.field', rule): +            proto = jmespath.search('rule.expr[0].match.right', rule).upper() + +        data_entries.append([rule_number, source, destination, proto, interface, translation]) + +    interface_header = 'Out-Int' if direction == 'source' else 'In-Int' +    headers = ["Rule", "Source", "Destination", "Proto", interface_header, "Translation"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def _get_formatted_output_statistics(data, direction): +    data_entries = [] +    for rule in data: +        if 'comment' in rule['rule']: +            comment = rule.get('rule').get('comment') +            rule_number = comment.split('-')[-1] +            rule_number = rule_number.split(' ')[0] +        if 'expr' in rule['rule']: +            interface = rule.get('rule').get('expr')[0].get('match').get('right') \ +                if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any' +            packets = jmespath.search('rule.expr[*].counter.packets | [0]', rule) +            _bytes = jmespath.search('rule.expr[*].counter.bytes | [0]', rule) +        data_entries.append([rule_number, packets, _bytes, interface]) +    headers = ["Rule", "Packets", "Bytes", "Interface"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def show_rules(raw: bool, direction: str): +    nat_rules = _get_raw_data_rules(direction) +    if raw: +        return nat_rules +    else: +        return _get_formatted_output_rules(nat_rules, direction) + + +def show_statistics(raw: bool, direction: str): +    nat_statistics = _get_raw_data_rules(direction) +    if raw: +        return nat_statistics +    else: +        return _get_formatted_output_statistics(nat_statistics, direction) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except ValueError as e: +        print(e) +        sys.exit(1) diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py deleted file mode 100755 index 3c7a90205..000000000 --- a/src/op_mode/show_vrf.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 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 jinja2 -from json import loads - -from vyos.util import cmd - -vrf_out_tmpl = """VRF name          state     mac address        flags                     interfaces ---------          -----     -----------        -----                     ---------- -{%- for v in vrf %} -{{"%-16s"|format(v.ifname)}}  {{ "%-8s"|format(v.operstate | lower())}}  {{"%-17s"|format(v.address | lower())}}  {{ v.flags|join(',')|lower()}}  {{v.members|join(',')|lower()}} -{%- endfor %} - -""" - -def list_vrfs(): -    command = 'ip -j -br link show type vrf' -    answer = loads(cmd(command)) -    return [_ for _ in answer if _] - -def list_vrf_members(vrf): -    command = f'ip -j -br link show master {vrf}' -    answer = loads(cmd(command)) -    return [_ for _ in answer if _] - -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-e", "--extensive", action="store_true", -                   help="provide detailed vrf informatio") -parser.add_argument('interface', metavar='I', type=str, nargs='?', -                    help='interface to display') - -args = parser.parse_args() - -if args.extensive: -    data = { 'vrf': [] } -    for vrf in list_vrfs(): -        name = vrf['ifname'] -        if args.interface and name != args.interface: -            continue - -        vrf['members'] = [] -        for member in list_vrf_members(name): -            vrf['members'].append(member['ifname']) -        data['vrf'].append(vrf) - -    tmpl = jinja2.Template(vrf_out_tmpl) -    print(tmpl.render(data)) - -else: -    print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 8955e5a59..68dc5bc45 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -87,6 +87,7 @@ def reset_profile(profile, tunnel):      print('Profile reset result: ' + ('success' if result == 0 else 'failed'))  def debug_peer(peer, tunnel): +    peer = peer.replace(':', '-')      if not peer or peer == "all":          debug_commands = [              "sudo ipsec statusall", @@ -109,7 +110,7 @@ def debug_peer(peer, tunnel):      if not tunnel or tunnel == 'all':          tunnel = '' -    conn = get_peer_connections(peer, tunnel) +    conns = get_peer_connections(peer, tunnel, return_all = (tunnel == '' or tunnel == 'all'))      if not conns:          print('Peer not found, aborting') diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py new file mode 100755 index 000000000..f86516786 --- /dev/null +++ b/src/op_mode/vrf.py @@ -0,0 +1,92 @@ +#!/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 +import sys +import typing + +from tabulate import tabulate +from vyos.util import cmd + +import vyos.opmode + + +def _get_raw_data(name=None): +    """ +    If vrf name is not set - get all VRFs +    If vrf name is set - get only this name data +    If vrf name set and not found - return [] +    """ +    output = cmd('sudo ip --json --brief link show type vrf') +    data = json.loads(output) +    if not data: +        return [] +    if name: +        is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False +        if is_vrf_exists: +            output = cmd(f'sudo ip --json --brief link show dev {name}') +            data = json.loads(output) +            return data +        return [] +    return data + + +def _get_vrf_members(vrf: str) -> list: +    """ +    Get list of interface VRF members +    :param vrf: str +    :return: list +    """ +    output = cmd(f'sudo ip --json --brief link show master {vrf}') +    answer = json.loads(output) +    interfaces = [] +    for data in answer: +        if 'ifname' in data: +            interfaces.append(data.get('ifname')) +    return interfaces if len(interfaces) > 0 else ['n/a'] + + +def _get_formatted_output(raw_data): +    data_entries = [] +    for vrf in raw_data: +        name = vrf.get('ifname') +        state = vrf.get('operstate').lower() +        hw_address = vrf.get('address') +        flags = ','.join(vrf.get('flags')).lower() +        members = ','.join(_get_vrf_members(name)) +        data_entries.append([name, state, hw_address, flags, members]) + +    headers = ["Name", "State", "MAC address", "Flags", "Interfaces"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + + +def show(raw: bool, name: typing.Optional[str]): +    vrf_data = _get_raw_data(name=name) +    if raw: +        return vrf_data +    else: +        return _get_formatted_output(vrf_data) + + +if __name__ == "__main__": +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except ValueError as e: +        print(e) +        sys.exit(1) | 
