diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2 index 0340d3c92..f6f03d0db 100644 --- a/data/templates/ids/fastnetmon.j2 +++ b/data/templates/ids/fastnetmon.j2 @@ -1,112 +1,121 @@ # enable this option if you want to send logs to local syslog facility logging:logging_level = debug 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 ## How many packets will be collected from attack traffic ban_details_records_count = 500 ## How long (in seconds) we should keep an IP in blocked state ## If you set 0 here it completely disables unban capability {% if ban_time is vyos_defined %} ban_time = {{ ban_time }} {% endif %} # Check if the attack is still active, before triggering an unban callback with this option # If the attack is still active, check each run of the unban watchdog unban_only_if_attack_finished = on # enable per subnet speed meters # For each subnet, list track speed in bps and pps for both directions enable_subnet_counters = off -{% if mode.mirror is vyos_defined %} +{% if mode is vyos_defined('mirror') %} mirror_afpacket = on +{% elif mode is vyos_defined('sflow') %} +sflow = on +{% if sflow.port is vyos_defined %} +sflow_port = {{ sflow.port }} +{% endif %} +{% if sflow.listen_address is vyos_defined %} +sflow_host = {{ sflow.listen_address }} +{% endif %} {% endif %} + process_incoming_traffic = {{ 'on' if direction is vyos_defined and 'in' in direction else 'off' }} process_outgoing_traffic = {{ 'on' if direction is vyos_defined and 'out' in direction else 'off' }} {% if threshold is vyos_defined %} {% if threshold.general is vyos_defined %} # General threshold {% for thr, thr_value in threshold.general.items() %} {% if thr is vyos_defined('fps') %} ban_for_flows = on threshold_flows = {{ thr_value }} {% elif thr is vyos_defined('mbps') %} ban_for_bandwidth = on threshold_mbps = {{ thr_value }} {% elif thr is vyos_defined('pps') %} ban_for_pps = on threshold_pps = {{ thr_value }} {% endif %} {% endfor %} {% endif %} {% if threshold.tcp is vyos_defined %} # TCP threshold {% for thr, thr_value in threshold.tcp.items() %} {% if thr is vyos_defined('fps') %} ban_for_tcp_flows = on threshold_tcp_flows = {{ thr_value }} {% elif thr is vyos_defined('mbps') %} ban_for_tcp_bandwidth = on threshold_tcp_mbps = {{ thr_value }} {% elif thr is vyos_defined('pps') %} ban_for_tcp_pps = on threshold_tcp_pps = {{ thr_value }} {% endif %} {% endfor %} {% endif %} {% if threshold.udp is vyos_defined %} # UDP threshold {% for thr, thr_value in threshold.udp.items() %} {% if thr is vyos_defined('fps') %} ban_for_udp_flows = on threshold_udp_flows = {{ thr_value }} {% elif thr is vyos_defined('mbps') %} ban_for_udp_bandwidth = on threshold_udp_mbps = {{ thr_value }} {% elif thr is vyos_defined('pps') %} ban_for_udp_pps = on threshold_udp_pps = {{ thr_value }} {% endif %} {% endfor %} {% endif %} {% if threshold.icmp is vyos_defined %} # ICMP threshold {% for thr, thr_value in threshold.icmp.items() %} {% if thr is vyos_defined('fps') %} ban_for_icmp_flows = on threshold_icmp_flows = {{ thr_value }} {% elif thr is vyos_defined('mbps') %} ban_for_icmp_bandwidth = on threshold_icmp_mbps = {{ thr_value }} {% elif thr is vyos_defined('pps') %} ban_for_icmp_pps = on threshold_icmp_pps = {{ thr_value }} {% endif %} {% endfor %} {% endif %} {% endif %} {% if listen_interface is vyos_defined %} interfaces = {{ listen_interface | join(',') }} {% endif %} {% if alert_script is vyos_defined %} notify_script_path = {{ alert_script }} {% endif %} diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2 index 5f1b3ba4d..0a0576d2a 100644 --- a/data/templates/ids/fastnetmon_networks_list.j2 +++ b/data/templates/ids/fastnetmon_networks_list.j2 @@ -1,5 +1,5 @@ -{% if network is vyos_defined() %} +{% if network is vyos_defined %} {% for net in 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 bb06189bc..78463136b 100644 --- a/interface-definitions/service-ids-ddos-protection.xml.in +++ b/interface-definitions/service-ids-ddos-protection.xml.in @@ -1,150 +1,167 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="service"> <children> <node name="ids"> <properties> <help>Intrusion Detection System</help> </properties> <children> <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_fastnetmon.py"> <properties> <help>FastNetMon detection and protection parameters</help> <priority>731</priority> </properties> <children> <leafNode name="alert-script"> <properties> <help>Path to fastnetmon alert script</help> </properties> </leafNode> <leafNode name="ban-time"> <properties> <help>How long we should keep an IP in blocked state</help> <valueHelp> <format>u32:1-4294967294</format> <description>Time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-4294967294"/> </constraint> </properties> <defaultValue>1900</defaultValue> </leafNode> <leafNode name="direction"> <properties> <help>Direction for processing traffic</help> <completionHelp> <list>in out</list> </completionHelp> <constraint> <regex>(in|out)</regex> </constraint> <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> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> <multi/> </properties> </leafNode> - <node name="mode"> + <leafNode name="mode"> <properties> - <help>Traffic capture modes</help> + <help>Traffic capture mode</help> + <completionHelp> + <list>mirror sflow</list> + </completionHelp> + <valueHelp> + <format>mirror</format> + <description>Listen to mirrored traffic</description> + </valueHelp> + <valueHelp> + <format>sflow</format> + <description>Capture sFlow flows</description> + </valueHelp> + <constraint> + <regex>(mirror|sflow)</regex> + </constraint> + </properties> + </leafNode> + <node name="sflow"> + <properties> + <help>Sflow settings</help> </properties> <children> - <!-- Future modes "mirror" "netflow" "combine (both)" --> - <leafNode name="mirror"> - <properties> - <help>Listen mirrored traffic mode</help> - <valueless/> - </properties> + #include <include/listen-address-ipv4-single.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>6343</defaultValue> </leafNode> </children> </node> <leafNode name="network"> <properties> <help>Specify IPv4 and IPv6 networks which belong to you</help> <valueHelp> <format>ipv4net</format> <description>Your IPv4 prefix(es)</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>Your IPv6 prefix(es)</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> <validator name="ipv6-prefix"/> </constraint> <multi/> </properties> </leafNode> <node name="threshold"> <properties> <help>Attack limits thresholds</help> </properties> <children> <node name="general"> <properties> <help>General threshold</help> </properties> <children> #include <include/ids/threshold.xml.i> </children> </node> <node name="tcp"> <properties> <help>TCP threshold</help> </properties> <children> #include <include/ids/threshold.xml.i> </children> </node> <node name="udp"> <properties> <help>UDP threshold</help> </properties> <children> #include <include/ids/threshold.xml.i> </children> </node> <node name="icmp"> <properties> <help>ICMP threshold</help> </properties> <children> #include <include/ids/threshold.xml.i> </children> </node> </children> </node> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index 2e678cf0b..f6b80552b 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -1,100 +1,108 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.utils.process import call from vyos.xml import defaults from vyos import ConfigError from vyos import airbag 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' +attack_dir = '/var/log/fastnetmon_attacks' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'ids', 'ddos-protection'] if not conf.exists(base): return None fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) fastnetmon = dict_merge(default_values, fastnetmon) return fastnetmon def verify(fastnetmon): if not fastnetmon: return None if 'mode' not in fastnetmon: raise ConfigError('Specify operating mode!') - if 'listen_interface' not in fastnetmon: - raise ConfigError('Specify interface(s) for traffic capture') + if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon: + raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring") + + if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}): + raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'") if 'alert_script' in fastnetmon: if os.path.isfile(fastnetmon['alert_script']): # Check script permissions if not os.access(fastnetmon['alert_script'], os.X_OK): raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script'])) else: raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon)) def generate(fastnetmon): if not fastnetmon: for file in [config_file, networks_list]: if os.path.isfile(file): os.unlink(file) return None + # Create dir for log attack details + if not os.path.exists(attack_dir): + os.mkdir(attack_dir) + 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): systemd_service = 'fastnetmon.service' if not fastnetmon: # Stop fastnetmon service if removed call(f'systemctl stop {systemd_service}') else: call(f'systemctl reload-or-restart {systemd_service}') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)