diff options
-rw-r--r-- | data/templates/ids/fastnetmon.j2 | 3 | ||||
-rw-r--r-- | data/templates/ids/fastnetmon_excluded_networks_list.j2 | 5 | ||||
-rw-r--r-- | interface-definitions/service-ids-ddos-protection.xml.in | 18 | ||||
-rw-r--r-- | op-mode-definitions/nat.xml.in | 2 | ||||
-rw-r--r-- | op-mode-definitions/show-vrf.xml.in | 2 | ||||
-rw-r--r-- | op-mode-definitions/vpn-ipsec.xml.in | 6 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_ids.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/service_ids_fastnetmon.py | 2 | ||||
-rwxr-xr-x | src/op_mode/ipsec.py | 71 | ||||
-rwxr-xr-x | src/op_mode/nat.py | 162 | ||||
-rwxr-xr-x | src/op_mode/vrf.py | 80 |
11 files changed, 358 insertions, 5 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..84e999995 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -16,7 +16,7 @@ <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> diff --git a/op-mode-definitions/show-vrf.xml.in b/op-mode-definitions/show-vrf.xml.in index 9c38c30fe..d8d5284d7 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> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index 928b74fd8..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> 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/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/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..f3f79f863 --- /dev/null +++ b/src/op_mode/nat.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 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): + 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 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) + + +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/vrf.py b/src/op_mode/vrf.py new file mode 100755 index 000000000..63d9b5ee5 --- /dev/null +++ b/src/op_mode/vrf.py @@ -0,0 +1,80 @@ +#!/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 + +from tabulate import tabulate +from vyos.util import cmd + +import vyos.opmode + + +def _get_raw_data(): + """ + :return: list + """ + output = cmd('sudo ip --json --brief link show type vrf') + data = json.loads(output) + 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): + vrf_data = _get_raw_data() + 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) |