| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package snmpsd |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | |
| 8 | "github.com/gosnmp/gosnmp" |
| 9 | |
| 10 | "github.com/netdata/netdata/go/plugins/pkg/confopt" |
| 11 | "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/iprange" |
| 12 | "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/snmputils" |
| 13 | ) |
| 14 | |
| 15 | type ( |
| 16 | Config struct { |
| 17 | Source string `yaml:"-" json:"-"` |
| 18 | |
| 19 | // RescanInterval defines how often to scan the networks for devices (default: 30m) |
| 20 | // Zero means use default. Negative means disable rescanning (run once). |
| 21 | RescanInterval confopt.LongDuration `yaml:"rescan_interval,omitempty" json:"rescan_interval,omitempty"` |
| 22 | // Timeout defines the maximum time to wait for SNMP device responses (default: 1s) |
| 23 | Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` |
| 24 | // DeviceCacheTTL defines how long to trust cached discovery results before requiring a new probe (default: 12h) |
| 25 | // Zero means use default. Negative means cache never expires. |
| 26 | DeviceCacheTTL confopt.LongDuration `yaml:"device_cache_ttl,omitempty" json:"device_cache_ttl,omitempty"` |
| 27 | // ParallelScansPerNetwork defines how many IPs to scan concurrently within each subnet (default: 32) |
| 28 | ParallelScansPerNetwork int `yaml:"parallel_scans_per_network,omitempty" json:"parallel_scans_per_network,omitempty"` |
| 29 | // Credentials define the SNMP credentials used for authentication |
| 30 | Credentials []CredentialConfig `yaml:"credentials,omitempty" json:"credentials,omitempty"` |
| 31 | // Networks defines the subnets to scan and which credentials to use |
| 32 | Networks []NetworkConfig `yaml:"networks,omitempty" json:"networks,omitempty"` |
| 33 | } |
| 34 | |
| 35 | NetworkConfig struct { |
| 36 | // Subnet is the IP range to scan, supporting various formats |
| 37 | // https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/pkg/iprange#supported-formats |
| 38 | Subnet string `yaml:"subnet" json:"subnet"` |
| 39 | // Credential is the name of a credential from the Credentials list |
| 40 | Credential string `yaml:"credential" json:"credential"` |
| 41 | } |
| 42 | CredentialConfig struct { |
| 43 | // Name is the identifier for this credential set, used in Network.Credential |
| 44 | Name string `yaml:"name" json:"name"` |
| 45 | // Version must be one of: "1", "2c", or "3" |
| 46 | Version string `yaml:"version" json:"version"` |
| 47 | // Community is the SNMP community string (used in v1 and v2c) |
| 48 | Community string `yaml:"community,omitempty" json:"community,omitempty"` |
| 49 | // UserName is the SNMPv3 username |
| 50 | UserName string `yaml:"username,omitempty" json:"username,omitempty"` |
| 51 | // SecurityLevel must be one of: "noAuthNoPriv", "authNoPriv", or "authPriv" (for SNMPv3) |
| 52 | SecurityLevel string `yaml:"security_level,omitempty" json:"security_level,omitempty"` |
| 53 | // AuthProtocol must be one of: "md5", "sha", "sha224", "sha256", "sha384", "sha512" (for SNMPv3) |
| 54 | AuthProtocol string `yaml:"auth_protocol,omitempty" json:"auth_protocol,omitempty"` |
| 55 | // AuthPassphrase is the authentication passphrase (for SNMPv3) |
| 56 | AuthPassphrase string `yaml:"auth_password,omitempty" json:"auth_password,omitempty"` |
| 57 | // PrivacyProtocol must be one of: "des", "aes", "aes192", "aes256", "aes192c", "aes256c" (for SNMPv3) |
| 58 | PrivacyProtocol string `yaml:"priv_protocol,omitempty" json:"priv_protocol,omitempty"` |
| 59 | // PrivacyPassphrase is the privacy passphrase (for SNMPv3) |
| 60 | PrivacyPassphrase string `yaml:"priv_password,omitempty" json:"priv_password,omitempty"` |
| 61 | // ContextName is the SNMPv3 context name. Empty means the default context. |
| 62 | ContextName string `yaml:"context_name,omitempty" json:"context_name,omitempty"` |
| 63 | } |
| 64 | ) |
| 65 | |
| 66 | func (c *Config) validateAndParse() ([]subnet, error) { |
| 67 | if len(c.Credentials) == 0 { |
| 68 | return nil, fmt.Errorf("no credentials provided") |
| 69 | } |
| 70 | if len(c.Networks) == 0 { |
| 71 | return nil, fmt.Errorf("no networks provided") |
| 72 | } |
| 73 | |
| 74 | credentials := make(map[string]CredentialConfig) |
| 75 | |
| 76 | for i, cr := range c.Credentials { |
| 77 | if cr.Name == "" { |
| 78 | return nil, fmt.Errorf("no name provided for credential %d", i) |
| 79 | } |
| 80 | if _, ok := credentials[cr.Name]; ok { |
| 81 | return nil, fmt.Errorf("duplicate credential name: %s", cr.Name) |
| 82 | } |
| 83 | credentials[cr.Name] = c.Credentials[i] |
| 84 | } |
| 85 | |
| 86 | networks := make(map[string]bool) |
| 87 | |
| 88 | var subnets []subnet |
| 89 | |
| 90 | for i, n := range c.Networks { |
| 91 | if n.Subnet == "" { |
| 92 | return nil, fmt.Errorf("no subnet provided for network %d", i) |
| 93 | } |
| 94 | if n.Credential == "" { |
| 95 | return nil, fmt.Errorf("no credential provided for network %s", n.Subnet) |
| 96 | } |
| 97 | if _, ok := credentials[n.Credential]; !ok { |
| 98 | return nil, fmt.Errorf("no credential provided for network %s", n.Subnet) |
| 99 | } |
| 100 | |
| 101 | r, err := iprange.ParseRange(n.Subnet) |
| 102 | if err != nil { |
| 103 | return nil, fmt.Errorf("invalid subnet range '%s': %v", n.Subnet, err) |
| 104 | } |
| 105 | |
| 106 | // Limit subnet size to /23 or smaller (512 IPs max per subnet) |
| 107 | // This prevents accidental scanning of excessively large networks. |
| 108 | if s := r.Size().Int64(); s > 512 { |
| 109 | return nil, fmt.Errorf("subnet '%s' exceeds maximum size of /23 (512 IPs, got %d IPs)", n.Subnet, s) |
| 110 | } |
| 111 | |
| 112 | sub := subnet{ |
| 113 | str: n.Subnet, |
| 114 | ips: r, |
| 115 | credential: credentials[n.Credential], |
| 116 | } |
| 117 | |
| 118 | if networks[subKey(sub)] { |
| 119 | return nil, fmt.Errorf("duplicate subnet '%s'", subKey(sub)) |
| 120 | } |
| 121 | networks[subKey(sub)] = true |
| 122 | |
| 123 | subnets = append(subnets, sub) |
| 124 | } |
| 125 | |
| 126 | return subnets, nil |
| 127 | } |
| 128 | |
| 129 | func setCredential(client gosnmp.Handler, cred CredentialConfig) { |
| 130 | switch snmputils.ParseSNMPVersion(cred.Version) { |
| 131 | case gosnmp.Version1: |
| 132 | client.SetVersion(gosnmp.Version1) |
| 133 | client.SetCommunity(cred.Community) |
| 134 | case gosnmp.Version2c: |
| 135 | client.SetVersion(gosnmp.Version2c) |
| 136 | client.SetCommunity(cred.Community) |
| 137 | case gosnmp.Version3: |
| 138 | client.SetVersion(gosnmp.Version3) |
| 139 | client.SetSecurityModel(gosnmp.UserSecurityModel) |
| 140 | client.SetMsgFlags(snmputils.ParseSNMPv3SecurityLevel(cred.SecurityLevel)) |
| 141 | client.SetSecurityParameters(&gosnmp.UsmSecurityParameters{ |
| 142 | UserName: cred.UserName, |
| 143 | AuthenticationProtocol: snmputils.ParseSNMPv3AuthProtocol(cred.AuthProtocol), |
| 144 | AuthenticationPassphrase: cred.AuthPassphrase, |
| 145 | PrivacyProtocol: snmputils.ParseSNMPv3PrivProtocol(cred.PrivacyProtocol), |
| 146 | PrivacyPassphrase: cred.PrivacyPassphrase, |
| 147 | }) |
| 148 | client.SetContextName(cred.ContextName) |
| 149 | } |
| 150 | } |