| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package panos |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | type ipsecResult struct { |
| 13 | NTun string `xml:"ntun"` |
| 14 | Entries *ipsecEntries `xml:"entries"` |
| 15 | } |
| 16 | |
| 17 | type ipsecEntries struct { |
| 18 | Entries []ipsecTunnel `xml:"entry"` |
| 19 | } |
| 20 | |
| 21 | type ipsecTunnel struct { |
| 22 | Name string `xml:"name"` |
| 23 | Gateway string `xml:"gateway"` |
| 24 | Remote string `xml:"remote"` |
| 25 | Protocol string `xml:"proto"` |
| 26 | Encryption string `xml:"enc"` |
| 27 | Remain string `xml:"remain"` |
| 28 | TID string `xml:"tid"` |
| 29 | ISPI string `xml:"i_spi"` |
| 30 | OSPI string `xml:"o_spi"` |
| 31 | } |
| 32 | |
| 33 | func (c *Collector) collectIPSecMetrics(ctx context.Context) (bool, error) { |
| 34 | body, err := c.apiClient.op(ctx, ipsecSACommand) |
| 35 | if err != nil { |
| 36 | return false, fmt.Errorf("ipsec metricset: %s API call: %w", panosCommandName(ipsecSACommand), err) |
| 37 | } |
| 38 | |
| 39 | payload, err := parseIPSecTunnels(body) |
| 40 | if err != nil { |
| 41 | return false, fmt.Errorf("ipsec metricset: %s response: %w", panosCommandName(ipsecSACommand), err) |
| 42 | } |
| 43 | if !payload.found { |
| 44 | return false, fmt.Errorf("ipsec metricset: %s response: %w", panosCommandName(ipsecSACommand), missingPANOSResultError{expected: "<ntun> or <entries>"}) |
| 45 | } |
| 46 | |
| 47 | c.metrics.ipsec.tunnelsActive.Observe(float64(payload.activeCount)) |
| 48 | |
| 49 | var errs []error |
| 50 | if payload.entriesFound && payload.activeCount != int64(len(payload.tunnels)) { |
| 51 | errs = append(errs, fmt.Errorf("IPsec active tunnel count mismatch: ntun=%d entries=%d; per-tunnel lifetime metrics may be incomplete", payload.activeCount, len(payload.tunnels))) |
| 52 | } |
| 53 | for _, tunnel := range payload.tunnels { |
| 54 | key := ipsecTunnelKey(tunnel) |
| 55 | value, err := parseRequiredPANOSIntField("IPsec tunnel "+firstNonEmpty(tunnel.Name, key)+" remain", tunnel.Remain) |
| 56 | if err != nil { |
| 57 | errs = append(errs, err) |
| 58 | continue |
| 59 | } |
| 60 | c.metrics.ipsec.saLifetime.WithLabelValues(ipsecTunnelLabelValues(tunnel)...).Observe(float64(value)) |
| 61 | } |
| 62 | return true, errors.Join(errs...) |
| 63 | } |
| 64 | |
| 65 | type ipsecTunnelPayload struct { |
| 66 | tunnels []ipsecTunnel |
| 67 | activeCount int64 |
| 68 | found bool |
| 69 | entriesFound bool |
| 70 | } |
| 71 | |
| 72 | func parseIPSecTunnels(body []byte) (ipsecTunnelPayload, error) { |
| 73 | var result ipsecResult |
| 74 | if err := decodePANOSResult(body, "PAN-OS IPsec response", &result); err != nil { |
| 75 | return ipsecTunnelPayload{}, err |
| 76 | } |
| 77 | if result.Entries == nil && strings.TrimSpace(result.NTun) == "" { |
| 78 | return ipsecTunnelPayload{}, nil |
| 79 | } |
| 80 | |
| 81 | payload := ipsecTunnelPayload{ |
| 82 | found: true, |
| 83 | entriesFound: result.Entries != nil, |
| 84 | } |
| 85 | var activeCount int64 |
| 86 | if strings.TrimSpace(result.NTun) != "" { |
| 87 | count, err := parseRequiredPANOSIntField("IPsec active tunnel count", result.NTun) |
| 88 | if err != nil { |
| 89 | return ipsecTunnelPayload{found: true, entriesFound: result.Entries != nil}, err |
| 90 | } |
| 91 | activeCount = count |
| 92 | } |
| 93 | if result.Entries == nil { |
| 94 | payload.activeCount = activeCount |
| 95 | return payload, nil |
| 96 | } |
| 97 | if strings.TrimSpace(result.NTun) == "" { |
| 98 | activeCount = int64(len(result.Entries.Entries)) |
| 99 | } |
| 100 | payload.tunnels = result.Entries.Entries |
| 101 | payload.activeCount = activeCount |
| 102 | return payload, nil |
| 103 | } |
| 104 | |
| 105 | func ipsecTunnelKey(tunnel ipsecTunnel) string { |
| 106 | return cleanID(firstNonEmpty(tunnel.Name, "unknown") + "_" + tunnel.Gateway + "_" + tunnel.Remote + "_" + firstNonEmpty(tunnel.TID, tunnel.ISPI, tunnel.OSPI)) |
| 107 | } |