From 6ca653e4200f18926a34bb82323931cc71f52e8a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 21 May 2018 16:21:28 +0200 Subject: T652: first SNMP version using XML interface definition --- src/conf_mode/snmp.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100755 src/conf_mode/snmp.py (limited to 'src') diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py new file mode 100755 index 000000000..d32a9a343 --- /dev/null +++ b/src/conf_mode/snmp.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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 . +# +# + +import sys +import os + +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +import vyos.version + +config_file_client = r'/etc/snmp/snmp.conf' +config_file_daemon = r'/etc/snmp/snmpd.conf' + +# SNMPS template - be careful if you edit the template. +client_config_tmpl = """ +### Autogenerated by snmp.py ### +{% if trap_source -%} +clientaddr {{ trap_source }} +{% endif %} + +""" + +# SNMPS template - be careful if you edit the template. +daemon_config_tmpl = """ +### Autogenerated by snmp.py ### +# non configurable defaults +sysObjectID 1.3.6.1.4.1.44641 +sysServices 14 +# maybe needed by lldpd +master agentx +agentXPerms 0755 0755 +# add hook to read IF-MIB::ifAlias from sysfs +pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias +# ospfd +smuxpeer .1.3.6.1.4.1.3317.1.2.2 +# bgpd +smuxpeer .1.3.6.1.4.1.3317.1.2.5 +# ripd +smuxpeer .1.3.6.1.4.1.3317.1.2.3 +# mribd +smuxpeer .1.3.6.1.4.1.3317.1.2.9 +# mribd +smuxpeer .1.3.6.1.2.1.83 +# pimd +smuxpeer .1.3.6.1.4.1.3317.1.2.8 +# pimd +smuxpeer .1.3.6.1.2.1.157 +smuxsocket localhost + +# linkUp/Down configure the Event MIB tables to monitor +# the ifTable for network interfaces being taken up or down +# for making internal queries to retrieve any necessary information +# create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' +## TODO!!!! +iquerySecName vyatta3392514e4189da84 + +# Modified from the default linkUpDownNotification +# to include more OIDs and poll more frequently +notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus +notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus +monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 +monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 + +######################## +# configurable section # +######################## + +# Version +sysDescr VyOS {{ version }} + +{% if description -%} +# Description +SysDescr {{ description }} +{% endif %} + +# Listen +agentaddress unix:/run/snmpd.socket{% for ip in listen_on %},udp:{{ ip.addr }}:{{ ip.port }}{% endfor %} + + +# SNMP communities +{% if communities -%} +{% for c in communities %} +{% if c.network -%} +{% for network in c.network %} +{{ c.authorization }}community {{ c.name }} {{ network }} +{% endfor %} +{% else %} +{{ c.authorization }}community {{ c.name }} +{% endif %} +{% endfor %} +{% endif %} + +{% if contact -%} +# system contact information +SysContact {{ contact }} +{% endif %} + +{% if location -%} +# system location information +SysLocation {{ location }} +{% endif %} + +{% if smux_peers -%} +# additional smux peers +{% for sp in smux_peers %} +smuxpeer {{ sp }} +{% endfor %} +{% endif %} + +""" + +default_config_data = { + 'listen_on': [], + 'location' : '', + 'description' : '', + 'contact' : '', + 'communities': [], + 'trap_source': '', + 'smux_peers': [] +} + +def get_config(): + snmp = default_config_data + conf = Config() + if not conf.exists('service snmp'): + return None + else: + conf.set_level('service snmp') + + version_data = vyos.version.get_version_data() + snmp.setdefault('version', version_data['version']) + + if conf.exists('community'): + for name in conf.list_nodes('community'): + community = { + 'name': name, + 'authorization': 'ro', + 'network': [] + } + + if conf.exists('community {0} authorization'.format(name)): + community['authorization'] = conf.return_value('community {0} authorization'.format(name)) + + if conf.exists('community {0} network'.format(name)): + community['network'] = conf.return_values('community {0} network'.format(name)) + + snmp['communities'].append(community) + + if conf.exists('contact'): + snmp['contact'] = conf.return_value('contact') + + if conf.exists('description'): + snmp['description'] = conf.return_value('description') + + if conf.exists('listen-address'): + for addr in conf.list_nodes('listen-address'): + listen = { + 'addr': addr, + 'port': '161' + } + + if conf.exists('listen-address {0} port'.format(addr)): + listen['port'] = conf.return_value('listen-address {0} port'.format(addr)) + + snmp['listen_on'].append(listen) + + if conf.exists('location'): + snmp['location'] = conf.return_value('location') + + if conf.exists('smux-peer'): + snmp['smux_peers'] = conf.return_values('smux-peer') + + if conf.exists('trap-source'): + snmp['trap_source'] = conf.return_value('trap-source') + + return snmp + +def verify(snmp): + return None + +def generate(snmp): + if snmp is None: + return None + + tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_client, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_daemon, 'w') as f: + f.write(config_text) + + return None + +def apply(snmp): + if snmp is not None: + os.system("sudo systemctl restart snmpd.service") + else: + # SNMP is removed in the commit + os.system("sudo systemctl stop snmpd.service") + os.unlink(config_file_client) + os.unlink(config_file_daemon) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From f12cab5ed3ffeb8a4c34cb874cc05a1cd381ba14 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 21 May 2018 19:43:00 +0200 Subject: T654: Support IPv6 configuration for SNMP listen address --- interface-definitions/snmp.xml | 8 ++++++++ src/conf_mode/snmp.py | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index bcd5295ee..698fb34a1 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -77,6 +77,14 @@ IP address to listen for incoming SNMP requests + + ipv4 + IPv4 address to listen for incoming SNMP requests + + + ipv6 + IPv6 address to listen for incoming SNMP requests + diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index d32a9a343..80f3bee8c 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,12 +20,12 @@ import sys import os import jinja2 +import vyos.version +import ipaddress from vyos.config import Config from vyos import ConfigError -import vyos.version - config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' @@ -92,7 +92,7 @@ SysDescr {{ description }} {% endif %} # Listen -agentaddress unix:/run/snmpd.socket{% for ip in listen_on %},udp:{{ ip.addr }}:{{ ip.port }}{% endfor %} +agentaddress unix:/run/snmpd.socket{% for ip in listen_on %},{{ ip.prot }}:{{ ip.addr }}:{{ ip.port }}{% endfor %} # SNMP communities @@ -172,8 +172,15 @@ def get_config(): if conf.exists('listen-address'): for addr in conf.list_nodes('listen-address'): + prot = "udp" + if ipaddress.ip_address(addr).version == 6: + # SNMP configuration file requires brackets on IPv6 addresses + addr = "[" + addr + "]" + prot = "udp6" + listen = { 'addr': addr, + 'prot': prot, 'port': '161' } -- cgit v1.2.3 From 0d72ed94a46a655cbc0ff9e32d258fbee3e9f330 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 21 May 2018 19:52:40 +0200 Subject: snmp.py: refactor listen-address config generation --- src/conf_mode/snmp.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 80f3bee8c..227c00335 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -92,7 +92,7 @@ SysDescr {{ description }} {% endif %} # Listen -agentaddress unix:/run/snmpd.socket{% for ip in listen_on %},{{ ip.prot }}:{{ ip.addr }}:{{ ip.port }}{% endfor %} +agentaddress unix:/run/snmpd.socket{% for li in listen_on %},{{ li }}{% endfor %} # SNMP communities @@ -172,20 +172,19 @@ def get_config(): if conf.exists('listen-address'): for addr in conf.list_nodes('listen-address'): - prot = "udp" - if ipaddress.ip_address(addr).version == 6: - # SNMP configuration file requires brackets on IPv6 addresses - addr = "[" + addr + "]" - prot = "udp6" - - listen = { - 'addr': addr, - 'prot': prot, - 'port': '161' - } - + listen = '' + port = '161' if conf.exists('listen-address {0} port'.format(addr)): - listen['port'] = conf.return_value('listen-address {0} port'.format(addr)) + port = conf.return_value('listen-address {0} port'.format(addr)) + + if ipaddress.ip_address(addr).version == 4: + # udp:127.0.0.1:161 + listen = 'udp:' + addr + ':' + port + elif ipaddress.ip_address(addr).version == 6: + # udp6:[::1]:161 + listen = 'udp6:' + '[' + addr + ']' + ':' + port + else: + raise ConfigError('Invalid IP address version') snmp['listen_on'].append(listen) -- cgit v1.2.3 From 1d9b22e1e80f0ae9a760feccaf6c2672adf2e33a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 21 May 2018 21:09:27 +0200 Subject: T652: read SNMPv3 config into python dictionary --- interface-definitions/snmp.xml | 55 +++++- src/conf_mode/snmp.py | 412 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 440 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index 698fb34a1..4bfb61576 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -20,7 +20,15 @@ - Authorization type (rw or ro) (default: 'ro') + Authorization type (default: 'ro') + + ro + read only + + + rw + read write + (ro|rw) @@ -202,10 +210,17 @@ Defines security level + + auth + Requests must be authenticated + + + priv + Enforce use of encryption + (auth|priv) - @@ -538,6 +553,42 @@ + + + Specifies the view with name viewname + + ^[^\(\)\|\-]+$ + + Illegal characters in name + + + + + Specifies the oid + + ^[0-9]+(\\.[0-9]+)*$ + + OID must start from a number + + + + + Exclude is an optional argument + + + + + Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant + + ^[0-9a-f]{2}([\\.:][0-9a-f]{2})*$ + + MASK is a list of hex octets, separated by '.' or ':' + + + + + + diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 227c00335..ef7ac12fc 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,14 +20,20 @@ import sys import os import jinja2 -import vyos.version import ipaddress +import random +import binascii +import os + +import vyos.version from vyos.config import Config from vyos import ConfigError config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' +config_file_access = r'/usr/share/snmp/snmpd.conf' +config_file_user = r'/var/lib/snmp/snmpd.conf' # SNMPS template - be careful if you edit the template. client_config_tmpl = """ @@ -38,39 +44,54 @@ clientaddr {{ trap_source }} """ +# SNMPS template - be careful if you edit the template. +access_config_tmpl = """ +### Autogenerated by snmp.py ### +{% if v3_users %} +{% for u in v3_users %} +{{ u.mode }}user {{ u.name }} +{% endfor %} +{% endif -%} +rwuser {{ vyos_user }} + +""" + +# SNMPS template - be careful if you edit the template. +user_config_tmpl = """ +### Autogenerated by snmp.py ### +# user +{% if v3_users %} +{% for u in v3_users %} +createUser {{ u.name }} {{ u.authProtocol | upper }} {% if u.authPassword %} "{{ u.authPassword }}" {% elif u.authMasterKey %} "{{ u.authMasterKey }}"{% endif %} {{ u.privProtocol | upper }}{% if u.privPassword %} {{ u.privPassword }}{% elif u.privMasterKey %} {{ u.privMasterKey }}{% endif %} +{% endfor %} +{% endif %} + +createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +""" + # SNMPS template - be careful if you edit the template. daemon_config_tmpl = """ ### Autogenerated by snmp.py ### + # non configurable defaults sysObjectID 1.3.6.1.4.1.44641 sysServices 14 -# maybe needed by lldpd master agentx agentXPerms 0755 0755 -# add hook to read IF-MIB::ifAlias from sysfs pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias -# ospfd +smuxpeer .1.3.6.1.2.1.83 +smuxpeer .1.3.6.1.2.1.157 smuxpeer .1.3.6.1.4.1.3317.1.2.2 -# bgpd -smuxpeer .1.3.6.1.4.1.3317.1.2.5 -# ripd smuxpeer .1.3.6.1.4.1.3317.1.2.3 -# mribd -smuxpeer .1.3.6.1.4.1.3317.1.2.9 -# mribd -smuxpeer .1.3.6.1.2.1.83 -# pimd +smuxpeer .1.3.6.1.4.1.3317.1.2.5 smuxpeer .1.3.6.1.4.1.3317.1.2.8 -# pimd -smuxpeer .1.3.6.1.2.1.157 +smuxpeer .1.3.6.1.4.1.3317.1.2.9 smuxsocket localhost # linkUp/Down configure the Event MIB tables to monitor # the ifTable for network interfaces being taken up or down # for making internal queries to retrieve any necessary information -# create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' -## TODO!!!! -iquerySecName vyatta3392514e4189da84 +iquerySecName {{ vyos_user }} # Modified from the default linkUpDownNotification # to include more OIDs and poll more frequently @@ -125,16 +146,75 @@ smuxpeer {{ sp }} {% endfor %} {% endif %} +{% if trap_targets -%} +# if there is a problem - tell someone! +{% for t in trap_targets %} +trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} +{% endfor %} +{% endif %} + +# +# SNMPv3 stuff goes here +# +{% if v3_enabled %} + +# views +{% if v3_views -%} +{% for v in v3_views %} +{% for oid in v.oids %} +view {{ v.name }} included .{{ oid.oid }} +{% endfor %} +{% endfor %} +{% endif %} + +# access +# context sec.model sec.level match read write notif +{% if v3_groups -%} +{% for g in v3_groups %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none +{% endfor -%} +{% endif %} + +# trap-target +{% if v3_traps -%} +{% for t in v3_traps %} +trapsess -v 3 -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} +{% endfor -%} +{% endif %} + +# group +{% if v3_users -%} +{% for u in v3_users %} +group {{ u.group }} usm {{ u.name }} +group {{ u.group }} tsm {{ u.name }} +{% endfor %} +{% endif %} + +{% endif %} + """ default_config_data = { 'listen_on': [], + 'communities': [], + 'smux_peers': [], 'location' : '', 'description' : '', 'contact' : '', - 'communities': [], 'trap_source': '', - 'smux_peers': [] + 'trap_targets': [], + 'vyos_user': '', + 'vyos_user_pass': '', + 'version': '999', + 'v3_enabled': 'False', + 'v3_engineid': '', + 'v3_groups': [], + 'v3_traps': [], + 'v3_tsm_key': '', + 'v3_tsm_port': '10161', + 'v3_users': [], + 'v3_views': [] } def get_config(): @@ -146,7 +226,12 @@ def get_config(): conf.set_level('service snmp') version_data = vyos.version.get_version_data() - snmp.setdefault('version', version_data['version']) + snmp['version'] = version_data['version'] + + # create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' + # os.urandom(8) returns 8 bytes of random data + snmp['vyos_user'] = 'vyatta' + binascii.hexlify(os.urandom(8)).decode('utf-8') + snmp['vyos_user_pass'] = binascii.hexlify(os.urandom(16)).decode('utf-8') if conf.exists('community'): for name in conf.list_nodes('community'): @@ -197,35 +282,312 @@ def get_config(): if conf.exists('trap-source'): snmp['trap_source'] = conf.return_value('trap-source') + if conf.exists('trap-target'): + for target in conf.list_nodes('trap-target'): + trap_tgt = { + 'target': target, + 'community': '', + 'port': '' + } + + if conf.exists('trap-target {0} community'.format(target)): + trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target)) + + if conf.exists('trap-target {0} port'.format(target)): + trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target)) + + snmp['trap_targets'].append(trap_tgt) + + ######################################################################### + # ____ _ _ __ __ ____ _____ # + # / ___|| \ | | \/ | _ \ __ _|___ / # + # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # + # ___) | |\ | | | | __/ \ V / ___) | # + # |____/|_| \_|_| |_|_| \_/ |____/ # + # # + # now take care about the fancy SNMP v3 stuff, or bail out eraly # + ######################################################################### + if not conf.exists('v3'): + return snmp + else: + snmp['v3_enabled'] = True + + # + # 'set service snmp v3 engineid' + # + if conf.exists('v3 engineid'): + snmp['v3_engineid'] = conf.return_value('v3 engineid') + + # + # 'set service snmp v3 group' + # + if conf.exists('v3 group'): + for group in conf.list_nodes('v3 group'): + v3_group = { + 'name': group, + 'mode': 'ro', + 'seclevel': 'auth', + 'view': '' + } + + if conf.exists('v3 group {0} mode'.format(group)): + v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group)) + + if conf.exists('v3 group {0} seclevel'.format(group)): + v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group)) + + if conf.exists('v3 group {0} view'.format(group)): + v3_group['view'] = conf.return_value('v3 group {0} view'.format(group)) + + snmp['v3_groups'].append(v3_group) + + # + # 'set service snmp v3 trap-target' + # + if conf.exists('v3 trap-target'): + for trap in conf.list_nodes('v3 trap-target'): + trap_cfg = { + 'ipAddr': trap, + 'engineID': '', + 'secName': '', + 'authProtocol': 'md5', + 'authPassword': '', + 'authMasterKey': '', + 'privProtocol': 'des', + 'privPassword': '', + 'privMasterKey': '', + 'ipProto': 'udp', + 'ipPort': '162', + 'type': '', + 'secLevel': 'noAuthNoPriv' + } + + if conf.exists('v3 trap-target {0} engineid'.format(trap)): + # Set the context engineID used for SNMPv3 REQUEST messages scopedPdu. + # If not specified, this will default to the authoritative engineID. + trap_cfg['engineID'] = conf.return_value('v3 trap-target {0} engineid'.format(trap)) + + if conf.exists('v3 trap-target {0} user'.format(trap)): + # Set the securityName used for authenticated SNMPv3 messages. + trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap)) + + if conf.exists('v3 trap-target {0} auth type'.format(trap)): + # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages + # cmdline option '-a' + trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap)) + + if conf.exists('v3 trap-target {0} auth plaintext-key'.format(trap)): + # Set the authentication pass phrase used for authenticated SNMPv3 messages. + # cmdline option '-A' + trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-key'.format(trap)) + + if conf.exists('v3 trap-target {0} auth encrypted-key'.format(trap)): + # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys. + # cmdline option '-3m' + trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-key'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy type'.format(trap)): + # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages. + # cmdline option '-x' + trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy plaintext-key'.format(trap)): + # Set the privacy pass phrase used for encrypted SNMPv3 messages. + # cmdline option '-X' + trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-key'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy encrypted-key'.format(trap)): + # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys. + # cmdline option '-3M' + trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-key'.format(trap)) + + if conf.exists('v3 trap-target {0} protocol'.format(trap)): + trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap)) + + if conf.exists('v3 trap-target {0} port'.format(trap)): + trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap)) + + if conf.exists('v3 trap-target {0} type'.format(trap)): + tmp = conf.return_value('v3 trap-target {0} type'.format(trap)) + # see http://www.net-snmp.org/docs/man/snmpd.conf.html + # The option -Ci can be used (with -v2c or -v3) to generate an INFORM + # notification rather than an unacknowledged TRAP. + if tmp == 'inform': + trap_cfg['type'] = ' -Ci' + + # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv). + # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv. + if trap_cfg['authPassword'] or trap_cfg['authMasterKey']: + if trap_cfg['privProtocol'] or trap_cfg['privPassword']: + trap_cfg['secLevel'] = 'authPriv' + else: + trap_cfg['secLevel'] = 'authNoPriv' + + snmp['v3_traps'].append(trap_cfg) + + # + # 'set service snmp v3 tsm' + # + if conf.exists('v3 tsm'): + if conf.exists('v3 tsm local-key'): + snmp['v3_tsm_key'] = conf.return_value('v3 tsm local-key') + + if conf.exists('v3 tsm port'): + snmp['v3_tsm_port'] = conf.return_value('v3 tsm port') + + # + # 'set service snmp v3 user' + # + if conf.exists('v3 user'): + for user in conf.list_nodes('v3 user'): + user_cfg = { + 'name': user, + 'authMasterKey': '', + 'authPassword': '', + 'authProtocol': 'md5', + 'engineID': '', + 'group': '', + 'mode': 'ro', + 'privMasterKey': '', + 'privPassword': '', + 'privTsmKey': '', + 'privProtocol': 'des' + } + + # + # v3 user {0} auth + # + if conf.exists('v3 user {0} auth encrypted-key'.format(user)): + user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user)) + + if conf.exists('v3 user {0} auth plaintext-key'.format(user)): + user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-key'.format(user)) + + if conf.exists('v3 user {0} auth type'.format(user)): + user_cfg['authProtocol'] = conf.return_value('v3 user {0} auth type'.format(user)) + + # + # v3 user {0} engineid + # + if conf.exists('v3 user {0} engineid'.format(user)): + user_cfg['engineID'] = conf.return_value('v3 user {0} engineid'.format(user)) + + # + # v3 user {0} group + # + if conf.exists('v3 user {0} group'.format(user)): + user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user)) + + # + # v3 user {0} mode + # + if conf.exists('v3 user {0} mode'.format(user)): + user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user)) + + # + # v3 user {0} privacy + # + if conf.exists('v3 user {0} privacy encrypted-key'.format(user)): + user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user)) + + if conf.exists('v3 user {0} privacy plaintext-key'.format(user)): + user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user)) + + if conf.exists('v3 user {0} privacy tsm-key'.format(user)): + user_cfg['privTsmKey'] = conf.return_value('v3 user {0} privacy tsm-key'.format(user)) + + if conf.exists('v3 user {0} privacy type'.format(user)): + user_cfg['privProtocol'] = conf.return_value('v3 user {0} privacy type'.format(user)) + + snmp['v3_users'].append(user_cfg) + + # + # 'set service snmp v3 view' + # + if conf.exists('v3 view'): + for view in conf.list_nodes('v3 view'): + view_cfg = { + 'name': view, + 'oids': [] + } + + if conf.exists('v3 view {0} oid'.format(view)): + for oid in conf.list_nodes('v3 view {0} oid'.format(view)): + oid_cfg = { + 'oid': oid + } + view_cfg['oids'].append(oid_cfg) + snmp['v3_views'].append(view_cfg) + return snmp def verify(snmp): + if snmp is None: + return None + + if 'v3_traps' in snmp.keys(): + for trap in snmp['v3_traps']: + if trap['authPassword'] and trap['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap auth') + if trap['privPassword'] and trap['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap privacy') + + if 'v3_users' in snmp.keys(): + for user in snmp['v3_users']: + if user['authPassword'] and user['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') + if user['privPassword'] and user['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') + + if 'v3_group' in snmp.keys(): + for group in snmp['v3_group']: + if not group['view']: + raise ConfigError('You must create a view first') + return None def generate(snmp): + # + # As we are manipulating the snmpd user database we have to stop it first! + # This is even save if service is going to be removed + os.system("sudo systemctl stop snmpd.service") + os.unlink(config_file_client) + os.unlink(config_file_daemon) + os.unlink(config_file_access) + os.unlink(config_file_user) + if snmp is None: return None + # Write client config file tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) config_text = tmpl.render(snmp) with open(config_file_client, 'w') as f: f.write(config_text) + # Write server config file tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) config_text = tmpl.render(snmp) with open(config_file_daemon, 'w') as f: f.write(config_text) + # Write access rights config file + tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_access, 'w') as f: + f.write(config_text) + + # Write access rights config file + tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_user, 'w') as f: + f.write(config_text) + return None def apply(snmp): if snmp is not None: os.system("sudo systemctl restart snmpd.service") - else: - # SNMP is removed in the commit - os.system("sudo systemctl stop snmpd.service") - os.unlink(config_file_client) - os.unlink(config_file_daemon) return None -- cgit v1.2.3 From 6900ac9213903f239f915624553d986f4fdbe0d4 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 4 Jun 2018 19:08:20 +0200 Subject: T652: Add SNMPv3 TSM handling and commit verification --- interface-definitions/snmp.xml | 10 ++- src/conf_mode/snmp.py | 151 +++++++++++++++++++++++++++++++++++------ 2 files changed, 136 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index 3edc22aa4..7928de5d7 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -402,17 +402,21 @@ - Specifies that the snmpd uses encryption + Specifies that SNMPv3 uses the Transport Security Model (TSM) - Defines the server certificate fingerprint or key-file name + Fingerprint of a TSM server certificate + + ^[0-9A-F]{2}(:[0-9A-F]{2}){19}$ + + Value can be finger print key or filename in /config/snmp/tls/certs - Defines the port for TSM (default: '10161') + Defines the port used for TSM (default: '10161') 1-65535 Numeric IP port diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index ef7ac12fc..429181550 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -18,12 +18,14 @@ import sys import os +import stat +import pwd import jinja2 import ipaddress import random import binascii -import os +import re import vyos.version @@ -104,7 +106,11 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 # configurable section # ######################## -# Version +{% if v3_tsm_key %} +[snmp] localCert {{ v3_tsm_key }} +{% endif %} + +# Default system description is VyOS version sysDescr VyOS {{ version }} {% if description -%} @@ -113,7 +119,7 @@ SysDescr {{ description }} {% endif %} # Listen -agentaddress unix:/run/snmpd.socket{% for li in listen_on %},{{ li }}{% endfor %} +agentaddress unix:/run/snmpd.socket{% for li in listen_on %},{{ li }}{% endfor %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %} # SNMP communities @@ -179,7 +185,7 @@ access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none # trap-target {% if v3_traps -%} {% for t in v3_traps %} -trapsess -v 3 -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} +trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} {% endfor -%} {% endif %} @@ -217,6 +223,10 @@ default_config_data = { 'v3_views': [] } +def rmfile(file): + if os.path.isfile(file): + os.unlink(file) + def get_config(): snmp = default_config_data conf = Config() @@ -408,12 +418,7 @@ def get_config(): trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap)) if conf.exists('v3 trap-target {0} type'.format(trap)): - tmp = conf.return_value('v3 trap-target {0} type'.format(trap)) - # see http://www.net-snmp.org/docs/man/snmpd.conf.html - # The option -Ci can be used (with -v2c or -v3) to generate an INFORM - # notification rather than an unacknowledged TRAP. - if tmp == 'inform': - trap_cfg['type'] = ' -Ci' + trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap)) # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv). # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv. @@ -444,14 +449,14 @@ def get_config(): 'name': user, 'authMasterKey': '', 'authPassword': '', - 'authProtocol': 'md5', + 'authProtocol': '', 'engineID': '', 'group': '', 'mode': 'ro', 'privMasterKey': '', 'privPassword': '', 'privTsmKey': '', - 'privProtocol': 'des' + 'privProtocol': '' } # @@ -525,24 +530,117 @@ def verify(snmp): if snmp is None: return None + # bail out early if SNMP v3 is not configured + if not snmp['v3_enabled']: + return None + + tsmKeyPattern = re.compile('^[0-9A-F]{2}(:[0-9A-F]{2}){19}$', re.IGNORECASE) + + if snmp['v3_tsm_key']: + if not tsmKeyPattern.match(snmp['v3_tsm_key']): + if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): + if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): + raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + + if 'v3_groups' in snmp.keys(): + for group in snmp['v3_groups']: + # + # A view must exist prior to mapping it into a group + # + if 'view' in group.keys(): + error = True + if 'v3_views' in snmp.keys(): + for view in snmp['v3_views']: + if view['name'] == group['view']: + error = False + if error: + raise ConfigError('You must create view "{0}" first'.format(group['view'])) + else: + raise ConfigError('"view" must be specified') + + if not 'mode' in group.keys(): + raise ConfigError('"mode" must be specified') + + if not 'seclevel' in group.keys(): + raise ConfigError('"seclevel" must be specified') + + if 'v3_traps' in snmp.keys(): for trap in snmp['v3_traps']: if trap['authPassword'] and trap['authMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap auth') + raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap auth') + + if trap['authPassword'] == '' and trap['authMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for trap auth') + if trap['privPassword'] and trap['privMasterKey']: - raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap privacy') + raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap privacy') + + if trap['privPassword'] == '' and trap['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for trap privacy') + + if not 'type' in trap.keys(): + raise ConfigError('v3 trap: "type" must be specified') + + if not 'authPassword' and 'authMasterKey' in trap.keys(): + raise ConfigError('v3 trap: "auth" must be specified') + + if not 'authProtocol' in trap.keys(): + raise ConfigError('v3 trap: "protocol" must be specified') + + if not 'privPassword' and 'privMasterKey' in trap.keys(): + raise ConfigError('v3 trap: "user" must be specified') + + if 'type' in trap.keys(): + if trap['type'] == 'trap' and trap['engineID'] == '': + raise ConfigError('must specify engineid if type is "trap"') + else: + raise ConfigError('"type" must be specified') + if 'v3_users' in snmp.keys(): for user in snmp['v3_users']: if user['authPassword'] and user['authMasterKey']: raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') + if user['privPassword'] and user['privMasterKey']: raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') - if 'v3_group' in snmp.keys(): - for group in snmp['v3_group']: - if not group['view']: - raise ConfigError('You must create a view first') + if user['privPassword'] == '' and user['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') + + if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '': + raise ConfigError('Must specify auth or tsm-key for user auth') + + if user['privProtocol'] == '': + raise ConfigError('Must specify privacy type') + + if user['mode'] == '': + raise ConfigError('Must specify user mode ro/rw') + + if user['privTsmKey']: + if not tsmKeyPattern.match(snmp['v3_tsm_key']): + if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): + if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): + raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') + + if user['group']: + # + # Group must exist prior to mapping it into a group + # + error = True + if 'v3_groups' in snmp.keys(): + for group in snmp['v3_groups']: + if group['name'] == user['group']: + error = False + if error: + raise ConfigError('You must create group "{0}" first'.format(user['group'])) + + + if 'v3_views' in snmp.keys(): + for view in snmp['v3_views']: + if not view['oids']: + raise ConfigError('Must configure an oid') return None @@ -551,10 +649,10 @@ def generate(snmp): # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed os.system("sudo systemctl stop snmpd.service") - os.unlink(config_file_client) - os.unlink(config_file_daemon) - os.unlink(config_file_access) - os.unlink(config_file_user) + rmfile(config_file_client) + rmfile(config_file_daemon) + rmfile(config_file_access) + rmfile(config_file_user) if snmp is None: return None @@ -587,6 +685,15 @@ def generate(snmp): def apply(snmp): if snmp is not None: + + if not os.path.exists('/config/snmp/tls'): + os.makedirs('/config/snmp/tls') + os.chmod('/config/snmp/tls', stat.S_IWUSR | stat.S_IRUSR) + # get uid for user 'snmp' + snmp_uid = pwd.getpwnam('snmp').pw_uid + os.chown('/config/snmp/tls', snmp_uid, -1) + + # start SNMP daemon os.system("sudo systemctl restart snmpd.service") return None -- cgit v1.2.3