Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Implement firewall ( pf stats ) metrics #30

Merged
merged 14 commits into from
Jul 21, 2024
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Gathering metrics for specific subsystems can be disabled with the following fla
- `--exporter.disable-wireguard` - Disable the scraping of Wireguard service. Defaults to `false`.
- `--exporter.disable-unbound` - Disable the scraping of Unbound service. Defaults to `false`.
- `--exporter.disable-openvpn` - Disable the scraping of OpenVPN service. Defaults to `false`.
- `--exporter.disable-firewall` - Disable the scraping of Firewall (pf) metrics. Defaults to `false`.

To disable the exporter metrics itself use the following flag:

Expand All @@ -190,6 +191,9 @@ Flags:
--[no-]exporter.disable-openvpn
Disable the scraping of OpenVPN service
($OPNSENSE_EXPORTER_DISABLE_OPENVPN)
--[no-]exporter.disable-firewall
Disable the scraping of Firewall (pf) metrics
($OPNSENSE_EXPORTER_DISABLE_FIREWALL)
--web.telemetry-path="/metrics"
Path under which to expose metrics.
--[no-]web.disable-exporter-metrics
Expand Down
15 changes: 15 additions & 0 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ opnsense_interfaces_input_errors_total | Counter | interface, device, type | Int
opnsense_interfaces_output_errors_total | Counter | interface, device, type | Interfaces | Output errors on this interface by interface name and device | n/a |
opnsense_interfaces_collisions_total | Counter | interface, device, type | Interfaces | Collisions on this interface by interface name and device | n/a |

### Firewall

| Metric Name | Type | Labels | Subsystem | Description | Disable Flag |
| --- | --- | --- | --- | --- | --- |
opnsense_firewall_in_ipv4_block_packets | Gauge | interface | Firewall | The number of IPv4 incoming packets that were blocked by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_in_ipv4_pass_packets | Gauge | interface | Firewall | The number of IPv4 incoming packets that were passed by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_out_ipv4_block_packets | Gauge | interface | Firewall | The number of IPv4 outgoing packets that were blocked by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_out_ipv4_pass_packets | Gauge | interface | Firewall | The number of IPv4 outgoing packets that were passed by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_in_ipv6_block_packets | Gauge | interface | Firewall | The number of IPv6 incoming packets that were blocked by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_in_ipv6_pass_packets | Gauge | interface | Firewall | The number of IPv6 incoming packets that were passed by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_out_ipv6_block_packets | Gauge | interface | Firewall | The number of IPv6 outgoing packets that were blocked by the firewall by interface | --exporter.disable-firewall |
opnsense_firewall_out_ipv6_pass_packets | Gauge | interface | Firewall | The number of IPv6 outgoing packets that were passed by the firewall by interface | --exporter.disable-firewall |



### ARP

![arp](assets/arp.png)
Expand Down
7 changes: 7 additions & 0 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
ProtocolSubsystem = "protocol"
OpenVPNSubsystem = "openvpn"
ServicesSubsystem = "services"
FirewallSubsystem = "firewall"
)

// CollectorInstance is the interface a service specific collectors must implement.
Expand Down Expand Up @@ -97,6 +98,12 @@ func WithoutUnboundCollector() Option {
return withoutCollectorInstance(UnboundDNSSubsystem)
}

// WithoutFirewallCollector Option
// removes the firewall (pf) collector from the list of collectors
func WithoutFirewallCollector() Option {
return withoutCollectorInstance(FirewallSubsystem)
}

// New creates a new Collector instance.
func New(client *opnsense.Client, log log.Logger, instanceName string, options ...Option) (*Collector, error) {
c := Collector{
Expand Down
5 changes: 3 additions & 2 deletions internal/collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func TestCollector(t *testing.T) {
"test",
log.NewNopLogger(),
)

if err != nil {
t.Errorf("expected no error, got %v", err)
}
Expand All @@ -29,10 +28,10 @@ func TestCollector(t *testing.T) {
WithoutCronCollector(),
WithoutUnboundCollector(),
WithoutWireguardCollector(),
WithoutFirewallCollector(),
}

collector, err := New(&client, log.NewNopLogger(), "test", collectOpts...)

if err != nil {
t.Errorf("expected no error when creating collector, got %v", err)
}
Expand All @@ -47,6 +46,8 @@ func TestCollector(t *testing.T) {
t.Errorf("expected unbound_dns collector to be removed")
case "wireguard":
t.Errorf("expected wireguard collector to be removed")
case "firewall":
t.Errorf("expected firewall collector to be removed")
}
}
}
123 changes: 123 additions & 0 deletions internal/collector/firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package collector

import (
"github.com/AthennaMind/opnsense-exporter/opnsense"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)

type firewallCollector struct {
log log.Logger
inIPv4PassPackets *prometheus.Desc
outIPv4PassPackets *prometheus.Desc
inIPv4BlockPackets *prometheus.Desc
outIPv4BlockPackets *prometheus.Desc

inIPv6PassPackets *prometheus.Desc
outIPv6PassPackets *prometheus.Desc
inIPv6BlockPackets *prometheus.Desc
outIPv6BlockPackets *prometheus.Desc

subsystem string
instance string
}

func init() {
collectorInstances = append(collectorInstances, &firewallCollector{
subsystem: FirewallSubsystem,
})
}

func (c *firewallCollector) Name() string {
return c.subsystem
}

func (c *firewallCollector) Register(namespace, instanceLabel string, log log.Logger) {
c.log = log
c.instance = instanceLabel
level.Debug(c.log).
Log("msg", "Registering collector", "collector", c.Name())

c.inIPv4PassPackets = buildPrometheusDesc(c.subsystem, "in_ipv4_pass_packets",
"The number of IPv4 incoming packets that were allowed to pass through the firewall by interface",
[]string{"interface"},
)

c.outIPv4PassPackets = buildPrometheusDesc(c.subsystem, "out_ipv4_pass_packets",
"The number of IPv4 outgoing packets that were allowed to pass through the firewall by interface",
[]string{"interface"},
)

c.inIPv4BlockPackets = buildPrometheusDesc(c.subsystem, "in_ipv4_block_packets",
"The number of IPv4 incoming packets that were blocked by the firewall by interface",
[]string{"interface"},
)

c.outIPv4BlockPackets = buildPrometheusDesc(c.subsystem, "out_ipv4_block_packets",
"The number of IPv4 outgoing packets that were blocked by the firewall by interface",
[]string{"interface"},
)

c.inIPv6PassPackets = buildPrometheusDesc(c.subsystem, "in_ipv6_pass_packets",
"The number of IPv6 incoming packets that were allowed to pass through the firewall by interface",
[]string{"interface"},
)

c.outIPv6PassPackets = buildPrometheusDesc(c.subsystem, "out_ipv6_pass_packets",
"The number of IPv6 outgoing packets that were allowed to pass through the firewall by interface",
[]string{"interface"},
)

c.inIPv6BlockPackets = buildPrometheusDesc(c.subsystem, "in_ipv6_block_packets",
"The number of IPv6 incoming packets that were blocked by the firewall by interface",
[]string{"interface"},
)

c.outIPv6BlockPackets = buildPrometheusDesc(c.subsystem, "out_ipv6_block_packets",
"The number of IPv6 outgoing packets that were blocked by the firewall by interface",
[]string{"interface"},
)
}

func (c *firewallCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.inIPv4PassPackets
ch <- c.outIPv4PassPackets
ch <- c.inIPv4BlockPackets
ch <- c.outIPv4BlockPackets

ch <- c.inIPv6PassPackets
ch <- c.outIPv6PassPackets
ch <- c.inIPv6BlockPackets
ch <- c.outIPv6BlockPackets
}

func (c *firewallCollector) Update(client *opnsense.Client, ch chan<- prometheus.Metric) *opnsense.APICallError {
data, err := client.FetchPFStatsByInterface()
if err != nil {
return err
}

for _, v := range data.Interfaces {
metricsValueMapping := map[*prometheus.Desc]int{
c.inIPv4PassPackets: v.In4PassPackets,
c.outIPv4PassPackets: v.Out4PassPackets,
c.inIPv4BlockPackets: v.In4BlockPackets,
c.outIPv4BlockPackets: v.Out4BlockPackets,
c.inIPv6PassPackets: v.In6PassPackets,
c.outIPv6PassPackets: v.Out6PassPackets,
c.inIPv6BlockPackets: v.In6BlockPackets,
c.outIPv6BlockPackets: v.Out6BlockPackets,
}
for metric, value := range metricsValueMapping {
ch <- prometheus.MustNewConstMetric(
metric,
prometheus.GaugeValue,
float64(value),
v.InterfaceName,
c.instance,
)
}
}
return nil
}
4 changes: 4 additions & 0 deletions internal/collector/gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package collector
import (
"github.com/AthennaMind/opnsense-exporter/opnsense"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)

Expand All @@ -29,6 +30,9 @@ func (c *gatewaysCollector) Name() string {
func (c *gatewaysCollector) Register(namespace, instanceLabel string, log log.Logger) {
c.log = log
c.instance = instanceLabel
level.Debug(c.log).
Log("msg", "Registering collector", "collector", c.Name())

c.status = buildPrometheusDesc(c.subsystem, "status",
"Status of the gateway by name and address (1 = up, 0 = down, 2 = unknown)",
[]string{"name", "address"},
Expand Down
6 changes: 6 additions & 0 deletions internal/options/collectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ var (
"exporter.disable-openvpn",
"Disable the scraping of OpenVPN service",
).Envar("OPNSENSE_EXPORTER_DISABLE_OPENVPN").Default("false").Bool()
firewallCollectorDisabled = kingpin.Flag(
"exporter.disable-firewall",
"Disable the scraping of the firewall (pf) metrics",
).Envar("OPNSENSE_EXPORTER_DISABLE_FIREWALL").Default("false").Bool()
)

// CollectorsDisableSwitch hold the enabled/disabled state of the collectors
Expand All @@ -32,6 +36,7 @@ type CollectorsDisableSwitch struct {
Wireguard bool
Unbound bool
OpenVPN bool
Firewall bool
}

// CollectorsSwitches returns configured instances of CollectorsDisableSwitch
Expand All @@ -42,5 +47,6 @@ func CollectorsSwitches() CollectorsDisableSwitch {
Wireguard: !*wireguardCollectorDisabled,
Unbound: !*unboundCollectorDisabled,
OpenVPN: !*openVPNCollectorDisabled,
Firewall: !*firewallCollectorDisabled,
}
}
8 changes: 4 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ func main() {
options.Init()

logger, err := options.Logger()

if err != nil {
fmt.Fprintf(os.Stderr, "error creating logger: %v\n", err)
os.Exit(1)
Expand All @@ -39,7 +38,6 @@ func main() {
Log("msg", "settings Go MAXPROCS", "procs", runtime.GOMAXPROCS(0))

opnsConfig, err := options.OPNSense()

if err != nil {
level.Error(logger).
Log("msg", "failed to assemble OPNsense configuration", "err", err)
Expand All @@ -51,7 +49,6 @@ func main() {
version,
logger,
)

if err != nil {
level.Error(logger).
Log("msg", "opnsense client build failed", "err", err)
Expand Down Expand Up @@ -90,9 +87,12 @@ func main() {
collectorOptionFuncs = append(collectorOptionFuncs, collector.WithoutArpTableCollector())
level.Info(logger).Log("msg", "arp collector disabled")
}
if !collectorsSwitches.Firewall {
collectorOptionFuncs = append(collectorOptionFuncs, collector.WithoutFirewallCollector())
level.Info(logger).Log("msg", "firewall collector disabled")
}

collectorInstance, err := collector.New(&opnsenseClient, logger, *options.InstanceLabel, collectorOptionFuncs...)

if err != nil {
level.Error(logger).
Log("msg", "failed to construct the collecotr", "err", err)
Expand Down
26 changes: 13 additions & 13 deletions opnsense/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ func NewClient(cfg options.OPNSenseConfig, userAgentVersion string, log log.Logg
gatewayLossRegex: gatewayLossRegex,
gatewayRTTRegex: gatewayRTTRegex,
endpoints: map[EndpointName]EndpointPath{
"services": "api/core/service/search",
"protocolStatistics": "api/diagnostics/interface/getProtocolStatistics",
"arp": "api/diagnostics/interface/search_arp",
"dhcpv4": "api/dhcpv4/leases/searchLease",
"openVPNInstances": "api/openvpn/instances/search",
"interfaces": "api/diagnostics/traffic/interface",
"systemInfo": "widgets/api/get.php?load=system%2Ctemperature",
"gatewaysStatus": "api/routes/gateway/status",
"unboundDNSStatus": "api/unbound/diagnostics/stats",
"cronJobs": "api/cron/settings/searchJobs",
"wireguardClients": "api/wireguard/service/show",
"healthCheck": "api/core/system/status",
"services": "api/core/service/search",
"interfaces": "api/diagnostics/traffic/interface",
"protocolStatistics": "api/diagnostics/interface/getProtocolStatistics",
"pfStatisticsByInterface": "api/diagnostics/firewall/pf_statistics/interfaces",
"arp": "api/diagnostics/interface/search_arp",
"dhcpv4": "api/dhcpv4/leases/searchLease",
"openVPNInstances": "api/openvpn/instances/search",
"gatewaysStatus": "api/routes/gateway/status",
"unboundDNSStatus": "api/unbound/diagnostics/stats",
"cronJobs": "api/cron/settings/searchJobs",
"wireguardClients": "api/wireguard/service/show",
"healthCheck": "api/core/system/status",
},
headers: map[string]string{
"Accept": "application/json",
Expand All @@ -93,7 +93,7 @@ func NewClient(cfg options.OPNSenseConfig, userAgentVersion string, log log.Logg
RootCAs: sslPool,
},
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 1 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
Expand Down
2 changes: 1 addition & 1 deletion opnsense/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (c *Client) FetchCronTable() (CronTable, *APICallError) {
path, ok := c.endpoints["cronJobs"]
if !ok {
return cronTable, &APICallError{
Endpoint: "cron",
Endpoint: "cronJobs",
Message: "endpoint not found",
StatusCode: 0,
}
Expand Down
52 changes: 52 additions & 0 deletions opnsense/firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package opnsense

type FirewallPFStat struct {
InterfaceName string `json:"interface,omitempty"` // We will populate this field with the key of the map
References int `json:"references"`

In4PassPackets int `json:"in4_pass_packets"`
In4BlockPackets int `json:"in4_block_packets"`
Out4PassPackets int `json:"out4_pass_packets"`
Out4BlockPackets int `json:"out4_block_packets"`

In6PassPackets int `json:"in6_pass_packets"`
In6BlockPackets int `json:"in6_block_packets"`
Out6PassPackets int `json:"out6_pass_packets"`
Out6BlockPackets int `json:"out6_block_packets"`
}

// firewallPFStatsResponse is the struct returned by the OPNsense API
// when requesting the firewwall statistics by interface. The response is weird json
// that have the interface name as key and the FirewallPFStats struct as value
type firewallPFStatsResponse struct {
Interface map[string]FirewallPFStat `json:"interfaces"`
}

type FirewallPFStats struct {
Interfaces []FirewallPFStat
}

func (c *Client) FetchPFStatsByInterface() (FirewallPFStats, *APICallError) {
var resp firewallPFStatsResponse
var data FirewallPFStats

url, ok := c.endpoints["pfStatisticsByInterface"]
if !ok {
return data, &APICallError{
Endpoint: "pfStatisticsByInterface",
Message: "endpoint not found in client endpoints",
StatusCode: 0,
}
}

err := c.do("GET", url, nil, &resp)
if err != nil {
return data, err
}

for k, v := range resp.Interface {
v.InterfaceName = k
data.Interfaces = append(data.Interfaces, v)
}
return data, nil
}
Loading
Loading