Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F117520910
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
20 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/data/templates/pmacct/override.conf.j2 b/data/templates/pmacct/override.conf.j2
index 213569ddc..44a100bb6 100644
--- a/data/templates/pmacct/override.conf.j2
+++ b/data/templates/pmacct/override.conf.j2
@@ -1,17 +1,17 @@
{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
[Unit]
After=
After=vyos-router.service
ConditionPathExists=
ConditionPathExists=/run/pmacct/uacctd.conf
[Service]
EnvironmentFile=
ExecStart=
ExecStart={{ vrf_command }}/usr/sbin/uacctd -f /run/pmacct/uacctd.conf
+ExecStop=/usr/libexec/vyos/system/uacctd_stop.py $MAINPID 60
WorkingDirectory=
WorkingDirectory=/run/pmacct
-PIDFile=
-PIDFile=/run/pmacct/uacctd.pid
Restart=always
RestartSec=10
+KillMode=mixed
diff --git a/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2
index 1370f8121..aae0a0619 100644
--- a/data/templates/pmacct/uacctd.conf.j2
+++ b/data/templates/pmacct/uacctd.conf.j2
@@ -1,80 +1,80 @@
# Genereated from VyOS configuration
daemonize: true
promisc: false
-pidfile: /run/pmacct/uacctd.pid
+syslog: daemon
uacctd_group: 2
uacctd_nl_size: 2097152
snaplen: {{ packet_length }}
aggregate: in_iface{{ ',out_iface' if enable_egress is vyos_defined }},src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows
{% set pipe_size = buffer_size | int *1024 *1024 %}
plugin_pipe_size: {{ pipe_size }}
{# We need an integer division (//) without any remainder or fraction #}
plugin_buffer_size: {{ pipe_size // 1000 }}
{% if syslog_facility is vyos_defined %}
syslog: {{ syslog_facility }}
{% endif %}
{% if disable_imt is not defined %}
imt_path: /tmp/uacctd.pipe
imt_mem_pools_number: 169
{% endif %}
{% set plugin = [] %}
{% if netflow.server is vyos_defined %}
{% for server in netflow.server %}
{% set nf_server_key = 'nf_' ~ server | dot_colon_to_dash %}
{% set _ = plugin.append('nfprobe['~ nf_server_key ~ ']') %}
{% endfor %}
{% endif %}
{% if sflow.server is vyos_defined %}
{% for server in sflow.server %}
{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
{% set _ = plugin.append('sfprobe[' ~ sf_server_key ~ ']') %}
{% endfor %}
{% endif %}
{% if disable_imt is not defined %}
{% set _ = plugin.append('memory') %}
{% endif %}
plugins: {{ plugin | join(',') }}
{% if netflow.server is vyos_defined %}
# NetFlow servers
{% for server, server_config in netflow.server.items() %}
{# # prevent pmacct syntax error when using IPv6 flow collectors #}
{% set nf_server_key = 'nf_' ~ server | dot_colon_to_dash %}
nfprobe_receiver[{{ nf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }}
nfprobe_version[{{ nf_server_key }}]: {{ netflow.version }}
{% if netflow.engine_id is vyos_defined %}
nfprobe_engine[{{ nf_server_key }}]: {{ netflow.engine_id }}
{% endif %}
{% if netflow.max_flows is vyos_defined %}
nfprobe_maxflows[{{ nf_server_key }}]: {{ netflow.max_flows }}
{% endif %}
{% if netflow.sampling_rate is vyos_defined %}
sampling_rate[{{ nf_server_key }}]: {{ netflow.sampling_rate }}
{% endif %}
{% if netflow.source_address is vyos_defined %}
nfprobe_source_ip[{{ nf_server_key }}]: {{ netflow.source_address | bracketize_ipv6 }}
{% endif %}
{% if netflow.timeout is vyos_defined %}
nfprobe_timeouts[{{ nf_server_key }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }}
{% endif %}
{% endfor %}
{% endif %}
{% if sflow.server is vyos_defined %}
# sFlow servers
{% for server, server_config in sflow.server.items() %}
{# # prevent pmacct syntax error when using IPv6 flow collectors #}
{% set sf_server_key = 'sf_' ~ server | dot_colon_to_dash %}
sfprobe_receiver[{{ sf_server_key }}]: {{ server | bracketize_ipv6 }}:{{ server_config.port }}
sfprobe_agentip[{{ sf_server_key }}]: {{ sflow.agent_address }}
{% if sflow.sampling_rate is vyos_defined %}
sampling_rate[{{ sf_server_key }}]: {{ sflow.sampling_rate }}
{% endif %}
{% if sflow.source_address is vyos_defined %}
sfprobe_source_ip[{{ sf_server_key }}]: {{ sflow.source_address | bracketize_ipv6 }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 71acd69fa..f29fc94fb 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -1,288 +1,320 @@
#!/usr/bin/env python3
#
# 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
import re
from sys import exit
from ipaddress import ip_address
from vyos.base import Warning
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configverify import verify_vrf
from vyos.ifconfig import Section
from vyos.template import render
from vyos.utils.process import call
from vyos.utils.process import cmd
+from vyos.utils.process import run
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
uacctd_conf_path = '/run/pmacct/uacctd.conf'
systemd_service = 'uacctd.service'
systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf'
nftables_nflog_table = 'raw'
nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'
egress_nftables_nflog_table = 'inet mangle'
egress_nftables_nflog_chain = 'FORWARD'
# get nftables rule dict for chain in table
def _nftables_get_nflog(chain, table):
# define list with rules
rules = []
# prepare regex for parsing rules
rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)'
rule_re = re.compile(rule_pattern)
# run nftables, save output and split it by lines
nftables_command = f'nft -a list chain {table} {chain}'
tmp = cmd(nftables_command, message='Failed to get flows list')
# parse each line and add information to list
for current_rule in tmp.splitlines():
if 'FLOW_ACCOUNTING_RULE' not in current_rule:
continue
current_rule_parsed = rule_re.search(current_rule)
if current_rule_parsed:
groups = current_rule_parsed.groupdict()
rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] })
# return list with rules
return rules
def _nftables_config(configured_ifaces, direction, length=None):
# define list of nftables commands to modify settings
nftable_commands = []
nftables_chain = nftables_nflog_chain
nftables_table = nftables_nflog_table
if direction == "egress":
nftables_chain = egress_nftables_nflog_chain
nftables_table = egress_nftables_nflog_table
# prepare extended list with configured interfaces
configured_ifaces_extended = []
for iface in configured_ifaces:
configured_ifaces_extended.append({ 'iface': iface })
# get currently configured interfaces with nftables rules
active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table)
# compare current active list with configured one and delete excessive interfaces, add missed
active_nflog_ifaces = []
for rule in active_nflog_rules:
interface = rule['interface']
if interface not in configured_ifaces:
table = rule['table']
handle = rule['handle']
nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}')
else:
active_nflog_ifaces.append({
'iface': interface,
})
# do not create new rules for already configured interfaces
for iface in active_nflog_ifaces:
if iface in active_nflog_ifaces and iface in configured_ifaces_extended:
configured_ifaces_extended.remove(iface)
# create missed rules
for iface_extended in configured_ifaces_extended:
iface = iface_extended['iface']
iface_prefix = "o" if direction == "egress" else "i"
rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
# Also add IPv6 ingres logging
if nftables_table == nftables_nflog_table:
nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
# change nftables
for command in nftable_commands:
cmd(command, raising=ConfigError)
+def _nftables_trigger_setup(operation: str) -> None:
+ """Add a dummy rule to unlock the main pmacct loop with a packet-trigger
+
+ Args:
+ operation (str): 'add' or 'delete' a trigger
+ """
+ # check if a chain exists
+ table_exists = False
+ if run('nft -snj list table ip pmacct') == 0:
+ table_exists = True
+
+ if operation == 'delete' and table_exists:
+ nft_cmd: str = 'nft delete table ip pmacct'
+ cmd(nft_cmd, raising=ConfigError)
+ if operation == 'add' and not table_exists:
+ nft_cmds: list[str] = [
+ 'nft add table ip pmacct',
+ 'nft add chain ip pmacct pmacct_out { type filter hook output priority raw - 50 \\; policy accept \\; }',
+ 'nft add rule ip pmacct pmacct_out oif lo ip daddr 127.0.254.0 counter log group 2 snaplen 1 queue-threshold 0 comment NFLOG_TRIGGER'
+ ]
+ for nft_cmd in nft_cmds:
+ cmd(nft_cmd, raising=ConfigError)
+
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
base = ['system', 'flow-accounting']
if not conf.exists(base):
return None
flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
# We have gathered the dict representation of the CLI, but there are
# default values which we need to conditionally update into the
# dictionary retrieved.
default_values = conf.get_config_defaults(**flow_accounting.kwargs,
recursive=True)
# delete individual flow type defaults - should only be added if user
# sets this feature
for flow_type in ['sflow', 'netflow']:
if flow_type not in flow_accounting and flow_type in default_values:
del default_values[flow_type]
flow_accounting = config_dict_merge(default_values, flow_accounting)
return flow_accounting
def verify(flow_config):
if not flow_config:
return None
# check if at least one collector is enabled
if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
'or not set "disable-imt" for flow-accounting!')
# Check if at least one interface is configured
if 'interface' not in flow_config:
raise ConfigError('Flow accounting requires at least one interface to ' \
'be configured!')
# check that all configured interfaces exists in the system
for interface in flow_config['interface']:
if interface not in Section.interfaces():
# Changed from error to warning to allow adding dynamic interfaces
# and interface templates
Warning(f'Interface "{interface}" is not presented in the system')
# check sFlow configuration
if 'sflow' in flow_config:
# check if at least one sFlow collector is configured
if 'server' not in flow_config['sflow']:
raise ConfigError('You need to configure at least one sFlow server!')
# check that all sFlow collectors use the same IP protocol version
sflow_collector_ipver = None
for server in flow_config['sflow']['server']:
if sflow_collector_ipver:
if sflow_collector_ipver != ip_address(server).version:
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
verify_vrf(flow_config)
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']:
if 'agent_address' in flow_config['sflow']:
if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
'server". You need to set the same IP version for both "agent-address" and '\
'all sFlow servers')
if 'agent_address' in flow_config['sflow']:
tmp = flow_config['sflow']['agent_address']
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 sflow source-address exist in the system
if 'source_address' in flow_config['sflow']:
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!')
# check NetFlow configuration
if 'netflow' in flow_config:
# check if vrf is defined for netflow
netflow_vrf = None
if 'vrf' in flow_config:
netflow_vrf = flow_config['vrf']
# check if at least one NetFlow collector is configured if NetFlow configuration is presented
if 'server' not in flow_config['netflow']:
raise ConfigError('You need to configure at least one NetFlow server!')
# Check if configured netflow source-address exist in the system
if 'source_address' in flow_config['netflow']:
if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf):
tmp = flow_config['netflow']['source_address']
raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
# Check if engine-id compatible with selected protocol version
if 'engine_id' in flow_config['netflow']:
v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
engine_id = flow_config['netflow']['engine_id']
version = flow_config['netflow']['version']
if flow_config['netflow']['version'] == '5':
regex_filter = re.compile(v5_filter)
if not regex_filter.search(engine_id):
raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\
f'together with NetFlow protocol version "{version}"!')
else:
regex_filter = re.compile(v9v10_filter)
if not regex_filter.search(flow_config['netflow']['engine_id']):
raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\
f'with NetFlow protocol version "{version}"!')
# return True if all checks were passed
return True
def generate(flow_config):
if not flow_config:
return None
render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config)
render(systemd_override, 'pmacct/override.conf.j2', flow_config)
# Reload systemd manager configuration
call('systemctl daemon-reload')
def apply(flow_config):
- action = 'restart'
# Check if flow-accounting was removed and define command
if not flow_config:
_nftables_config([], 'ingress')
_nftables_config([], 'egress')
# Stop flow-accounting daemon and remove configuration file
call(f'systemctl stop {systemd_service}')
if os.path.exists(uacctd_conf_path):
os.unlink(uacctd_conf_path)
+
+ # must be done after systemctl
+ _nftables_trigger_setup('delete')
+
return
# Start/reload flow-accounting daemon
call(f'systemctl restart {systemd_service}')
# configure nftables rules for defined interfaces
if 'interface' in flow_config:
_nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length'])
# configure egress the same way if configured otherwise remove it
if 'enable_egress' in flow_config:
_nftables_config(flow_config['interface'], 'egress', flow_config['packet_length'])
else:
_nftables_config([], 'egress')
+ # add a trigger for signal processing
+ _nftables_trigger_setup('add')
+
+
if __name__ == '__main__':
try:
config = get_config()
verify(config)
generate(config)
apply(config)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/system/uacctd_stop.py b/src/system/uacctd_stop.py
new file mode 100755
index 000000000..7fbac0566
--- /dev/null
+++ b/src/system/uacctd_stop.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Control pmacct daemons in a tricky way.
+# Pmacct has signal processing in a main loop, together with packet
+# processing. Because of this, while it is waiting for packets, it cannot
+# handle the control signal. We need to start the systemctl command and then
+# send some packets to pmacct to wake it up
+
+from argparse import ArgumentParser
+from socket import socket
+from sys import exit
+from time import sleep
+
+from psutil import Process
+
+
+def stop_process(pid: int, timeout: int) -> None:
+ """Send a signal to uacctd
+ and then send packets to special address predefined in a firewall
+ to unlock main loop in uacctd and finish the process properly
+
+ Args:
+ pid (int): uacctd PID
+ timeout (int): seconds to wait for a process end
+ """
+ # find a process
+ uacctd = Process(pid)
+ uacctd.terminate()
+
+ # create a socket
+ trigger = socket()
+
+ first_cycle: bool = True
+ while uacctd.is_running() and timeout:
+ trigger.sendto(b'WAKEUP', ('127.0.254.0', 0))
+ # do not sleep during first attempt
+ if not first_cycle:
+ sleep(1)
+ timeout -= 1
+ first_cycle = False
+
+
+if __name__ == '__main__':
+ parser = ArgumentParser()
+ parser.add_argument('process_id',
+ type=int,
+ help='PID file of uacctd core process')
+ parser.add_argument('timeout',
+ type=int,
+ help='time to wait for process end')
+ args = parser.parse_args()
+ stop_process(args.process_id, args.timeout)
+ exit()
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 26, 12:53 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4285191
Default Alt Text
(20 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment